diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6fddc5a7..a107caa8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -154,6 +154,9 @@ cronUtils = "com.cronutils:cron-utils:9.2.1" # Webview kcef = "dev.datlag:kcef:2024.04.20.4" +# User +jwt = "com.auth0:java-jwt:4.4.0" + # lint - used for renovate to update ktlint version ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index fc2f47c5..8bb12695 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -102,6 +102,8 @@ dependencies { implementation(libs.cronUtils) + implementation(libs.jwt) + compileOnly(libs.kte) } diff --git a/server/i18n/src/commonMain/moko-resources/values/base/strings.xml b/server/i18n/src/commonMain/moko-resources/values/base/strings.xml index cad690ec..c92603d4 100644 --- a/server/i18n/src/commonMain/moko-resources/values/base/strings.xml +++ b/server/i18n/src/commonMain/moko-resources/values/base/strings.xml @@ -142,6 +142,7 @@ Loading page... Copy to Clipboard Automatic clipboard copy failed, please use the input below to manually copy the value. + Your configuration requires you to login. Please enter username and password. Enter URL... Suwayomi Login diff --git a/server/src/main/jte/Webview.kte b/server/src/main/jte/Webview.kte index f4b541c8..f199d7ff 100644 --- a/server/src/main/jte/Webview.kte +++ b/server/src/main/jte/Webview.kte @@ -159,25 +159,26 @@ main .contextmenu button:hover { background: #eee; } - .copydialog { + .copydialog, .logindialog { display: none; position: absolute; inset: 0; width: 100%; height: 100%; padding: 6px; + z-index: 1; } - .copydialog.show { + .copydialog.show, .logindialog.show { display: block; } - .copydialog::before { + .copydialog::before, .logindialog::before { content: ''; position: absolute; inset: 0; background: black; opacity: 0.3; } - .copydialog__inner { + .copydialog__inner, .logindialog__inner { position: relative; max-width: 960px; border-radius: 8px; @@ -204,10 +205,10 @@ line-height: 1; } @media (min-width: 500px) { - .copydialog { + .copydialog, .logindialog { padding: 24px; } - .copydialog__inner { + .copydialog__inner, .logindialog__inner { padding: 12px 18px; height: auto; } @@ -222,6 +223,86 @@ border-bottom: 9px solid transparent; border-left: 9px solid currentcolor; } + + .logindialog .error { + margin: 8px; + padding: 8px 16px; + border-radius: 8px; + border: 1px solid #b71c1c; + background-color: #c62828; + color: white; + } + .logindialog .error:empty { + display: none; + } + .logindialog form label { + cursor: pointer; + } + .logindialog form button { + all: unset; + padding: 8px; + line-height: 1.75; + text-align: center; + min-width: 64px; + border-radius: 4px; + padding: 6px 8px; + color: rgb(91, 116, 239); + text-transform: uppercase; + letter-spacing: 0.02857em; + } + .logindialog form button:not([disabled]) { + cursor: pointer; + } + .logindialog form button:not([disabled]):hover { + background-color: rgba(91, 116, 239, 0.08); + } + .logindialog form input { + all: unset; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.23); + padding: 6px 12px; + width: auto; + min-width: 0; + } + .logindialog form input:hover { + border-color: white; + } + .logindialog form input:focus { + border-color: rgb(91, 116, 239); + } + .logindialog form .controls { + display: grid; + align-items: center; + grid-template-columns: 1fr; + } + .logindialog form .controls > :nth-child(even):not(:last-child) { + margin-bottom: 6px; + } + .logindialog form .submit { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 24px; + } + .logindialog input:disabled, .logindialog button:disabled { + opacity: 0.7; + } + + @media (min-width: 500px) { + .logindialog form { + width: 100%; + max-width: 450px; + margin: 8px auto; + } + .logindialog form .controls { + grid-template-columns: auto 1fr; + column-gap: 16px; + row-gap: 6px; + } + .logindialog form .controls > :nth-child(even):not(:last-child) { + margin-bottom: 0px; + } + } @@ -256,6 +337,24 @@ + diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/GlobalMetaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/GlobalMetaController.kt index d3c9da40..42d32615 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/GlobalMetaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/GlobalMetaController.kt @@ -9,6 +9,9 @@ package suwayomi.tachidesk.global.controller import io.javalin.http.HttpStatus import suwayomi.tachidesk.global.impl.GlobalMeta +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.withOperation @@ -24,6 +27,7 @@ object GlobalMetaController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(GlobalMeta.getMetaMap()) ctx.status(200) }, @@ -44,6 +48,7 @@ object GlobalMetaController { } }, behaviorOf = { ctx, key, value -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() GlobalMeta.modifyMeta(key, value) ctx.status(200) }, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt index c456f517..f419546a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt @@ -12,7 +12,10 @@ import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.AboutDataClass import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.UpdateDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.withOperation @@ -28,6 +31,7 @@ object SettingsController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(About.getAbout()) }, withResults = { @@ -45,6 +49,7 @@ object SettingsController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { AppUpdate.checkUpdate() } .thenApply { ctx.json(it) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/WebViewController.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/WebViewController.kt index 2b63aaef..feeb068b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/WebViewController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/WebViewController.kt @@ -12,6 +12,9 @@ import io.javalin.http.HttpStatus import io.javalin.websocket.WsConfig import suwayomi.tachidesk.global.impl.WebView import suwayomi.tachidesk.i18n.LocalizationHelper +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.withOperation @@ -28,6 +31,7 @@ object WebViewController { } }, behaviorOf = { ctx, lang -> + // intentionally not user-protected, this pages handles login by itself val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.contentType(ContentType.TEXT_HTML) ctx.render( @@ -41,8 +45,15 @@ object WebViewController { ) fun webviewWS(ws: WsConfig) { - ws.onConnect { ctx -> WebView.addClient(ctx) } - ws.onMessage { ctx -> WebView.handleRequest(ctx) } - ws.onClose { ctx -> WebView.removeClient(ctx) } + ws.onConnect { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() + WebView.addClient(ctx) + } + ws.onMessage { ctx -> + WebView.handleRequest(ctx) + } + ws.onClose { ctx -> + WebView.removeClient(ctx) + } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt index d98e7e72..08514645 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt @@ -89,6 +89,10 @@ object WebView : Websocket() { @SerialName("copy") class JsCopyMessage : TypeObject() + @Serializable + @SerialName("ping") + class JsPingMessage : TypeObject() + override fun handleRequest(ctx: WsMessageContext) { val dr = driver ?: return try { @@ -113,6 +117,9 @@ object WebView : Websocket() { is JsCopyMessage -> { dr.copy() } + is JsPingMessage -> { + notifyAllClients("{\"type\":\"pong\"}") + } } } catch (e: Exception) { logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt new file mode 100644 index 00000000..90e1fa2b --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt @@ -0,0 +1,126 @@ +package suwayomi.tachidesk.global.impl.util + +import android.app.Application +import android.content.Context +import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.JWTVerificationException +import io.github.oshai.kotlinlogging.KotlinLogging +import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.UserType +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.security.SecureRandom +import java.time.Instant +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +object Jwt { + private val preferenceStore = + Injekt.get().getSharedPreferences("jwt", Context.MODE_PRIVATE) + private val logger = KotlinLogging.logger {} + + private const val ALGORITHM = "HmacSHA256" + private val accessTokenExpiry get() = serverConfig.jwtTokenExpiry.value + private val refreshTokenExpiry get() = serverConfig.jwtRefreshExpiry.value + private const val ISSUER = "suwayomi-server" + private val AUDIENCE get() = serverConfig.jwtAudience.value + + private const val PREF_KEY = "jwt_key" + + @OptIn(ExperimentalEncodingApi::class) + fun generateSecret(): String { + val byteString = preferenceStore.getString(PREF_KEY, "") + val decodedKeyBytes = + try { + Base64.Default.decode(byteString) + } catch (e: IllegalArgumentException) { + logger.warn(e) { "Invalid key specified, regenerating" } + null + } + + val keyBytes = + if (decodedKeyBytes?.size == 32) { + decodedKeyBytes + } else { + val k = ByteArray(32) + SecureRandom().nextBytes(k) + preferenceStore.edit().putString(PREF_KEY, Base64.Default.encode(k)).apply() + k + } + + val secretKey = SecretKeySpec(keyBytes, ALGORITHM) + + return Base64.encode(secretKey.encoded) + } + + private val algorithm: Algorithm = Algorithm.HMAC256(generateSecret()) + private val verifier: JWTVerifier = JWT.require(algorithm).build() + + class JwtTokens( + val accessToken: String, + val refreshToken: String, + ) + + fun generateJwt(): JwtTokens { + val accessToken = createAccessToken() + val refreshToken = createRefreshToken() + + return JwtTokens( + accessToken = accessToken, + refreshToken = refreshToken, + ) + } + + fun refreshJwt(refreshToken: String): String { + val jwt = verifier.verify(refreshToken) + require(jwt.getClaim("token_type").asString() == "refresh") { + "Cannot use access token to refresh" + } + require(jwt.audience.single() == AUDIENCE) { + "Token intended for different audience ${jwt.audience}" + } + return createAccessToken() + } + + fun verifyJwt(jwt: String): UserType { + try { + val decodedJWT = verifier.verify(jwt) + + require(decodedJWT.getClaim("token_type").asString() == "access") { + "Cannot use refresh token to access" + } + require(decodedJWT.audience.single() == AUDIENCE) { + "Token intended for different audience ${decodedJWT.audience}" + } + + return UserType.Admin(1) + } catch (e: JWTVerificationException) { + logger.warn(e) { "Received invalid token" } + return UserType.Visitor + } + } + + private fun createAccessToken(): String { + val jwt = + JWT + .create() + .withIssuer(ISSUER) + .withAudience(AUDIENCE) + .withClaim("token_type", "access") + .withExpiresAt(Instant.now().plusSeconds(accessTokenExpiry.inWholeSeconds)) + + return jwt.sign(algorithm) + } + + private fun createRefreshToken(): String = + JWT + .create() + .withIssuer(ISSUER) + .withAudience(AUDIENCE) + .withClaim("token_type", "refresh") + .withExpiresAt(Instant.now().plusSeconds(refreshTokenExpiry.inWholeSeconds)) + .sign(algorithm) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt index 3590e7c6..3f858b5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt @@ -1,16 +1,21 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment import io.javalin.http.UploadedFile import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.server.TemporaryFileStorage +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.BackupRestoreStatus import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -26,7 +31,11 @@ class BackupMutation { val status: BackupRestoreStatus?, ) - fun restoreBackup(input: RestoreBackupInput): CompletableFuture { + fun restoreBackup( + dataFetchingEnvironment: DataFetchingEnvironment, + input: RestoreBackupInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, backup) = input return future { @@ -53,7 +62,11 @@ class BackupMutation { val url: String, ) - fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload { + fun createBackup( + dataFetchingEnvironment: DataFetchingEnvironment, + input: CreateBackupInput? = null, + ): CreateBackupPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val filename = Backup.getFilename() val backup = diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt index 70fb7d31..c3d0c802 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus @@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.CategoryMetaType import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.MangaType @@ -23,6 +25,9 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser class CategoryMutation { data class SetCategoryMetaInput( @@ -35,8 +40,12 @@ class CategoryMutation { val meta: CategoryMetaType, ) - fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult = + fun setCategoryMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetCategoryMetaInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input Category.modifyMeta(meta.categoryId, meta.key, meta.value) @@ -56,8 +65,12 @@ class CategoryMutation { val category: CategoryType, ) - fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult = + fun deleteCategoryMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteCategoryMetaInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId, key) = input val (meta, category) = @@ -150,8 +163,12 @@ class CategoryMutation { } } - fun updateCategory(input: UpdateCategoryInput): DataFetcherResult = + fun updateCategory( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input updateCategories(listOf(id), patch) @@ -167,8 +184,12 @@ class CategoryMutation { ) } - fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult = + fun updateCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoriesInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input updateCategories(ids, patch) @@ -195,8 +216,12 @@ class CategoryMutation { val position: Int, ) - fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult = + fun updateCategoryOrder( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryOrderInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId, position) = input require(position > 0) { "'order' must not be <= 0" @@ -253,8 +278,12 @@ class CategoryMutation { val category: CategoryType, ) - fun createCategory(input: CreateCategoryInput): DataFetcherResult = + fun createCategory( + dataFetchingEnvironment: DataFetchingEnvironment, + input: CreateCategoryInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input transaction { require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) { @@ -312,8 +341,12 @@ class CategoryMutation { val mangas: List, ) - fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult { + fun deleteCategory( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteCategoryInput, + ): DataFetcherResult { return asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId) = input if (categoryId == 0) { // Don't delete default category return@asDataFetcherResult DeleteCategoryPayload( @@ -401,8 +434,12 @@ class CategoryMutation { } } - fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult = + fun updateMangaCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangaCategoriesInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input updateMangas(listOf(id), patch) @@ -418,8 +455,12 @@ class CategoryMutation { ) } - fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult = + fun updateMangasCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangasCategoriesInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input updateMangas(ids, patch) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt index a73fd8b5..9b90c513 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ChapterMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.jetbrains.exposed.dao.id.EntityID @@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.statements.BatchUpdateStatement import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.SyncConflictInfoType @@ -20,7 +22,10 @@ import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.net.URLEncoder import java.time.Instant import java.util.concurrent.CompletableFuture @@ -112,8 +117,12 @@ class ChapterMutation { } } - fun updateChapter(input: UpdateChapterInput): DataFetcherResult = + fun updateChapter( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateChapterInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input updateChapters(listOf(id), patch) @@ -129,8 +138,12 @@ class ChapterMutation { ) } - fun updateChapters(input: UpdateChaptersInput): DataFetcherResult = + fun updateChapters( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateChaptersInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input updateChapters(ids, patch) @@ -156,7 +169,11 @@ class ChapterMutation { val chapters: List, ) - fun fetchChapters(input: FetchChaptersInput): CompletableFuture> { + fun fetchChapters( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchChaptersInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId) = input return future { @@ -190,8 +207,12 @@ class ChapterMutation { val meta: ChapterMetaType, ) - fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult = + fun setChapterMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetChapterMetaInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value) @@ -211,8 +232,12 @@ class ChapterMutation { val chapter: ChapterType, ) - fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult = + fun deleteChapterMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteChapterMetaInput, + ): DataFetcherResult = asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapterId, key) = input val (meta, chapter) = @@ -260,7 +285,11 @@ class ChapterMutation { val syncConflict: SyncConflictInfoType?, ) - fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture> { + fun fetchChapterPages( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchChapterPagesInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapterId) = input val paramsMap = input.toParams() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt index 34edd441..fee5c003 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt @@ -1,11 +1,13 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.manga.impl.Chapter @@ -13,7 +15,10 @@ import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.model.DownloadUpdateType.DEQUEUED import suwayomi.tachidesk.manga.impl.download.model.Status import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -28,7 +33,11 @@ class DownloadMutation { val chapters: List, ) - fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult { + fun deleteDownloadedChapters( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteDownloadedChaptersInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input return asDataFetcherResult { @@ -57,7 +66,11 @@ class DownloadMutation { val chapters: ChapterType, ) - fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult { + fun deleteDownloadedChapter( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteDownloadedChapterInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input return asDataFetcherResult { @@ -84,8 +97,10 @@ class DownloadMutation { ) fun enqueueChapterDownloads( + dataFetchingEnvironment: DataFetchingEnvironment, input: EnqueueChapterDownloadsInput, ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input return future { @@ -118,7 +133,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture> { + fun enqueueChapterDownload( + dataFetchingEnvironment: DataFetchingEnvironment, + input: EnqueueChapterDownloadInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input return future { @@ -151,8 +170,10 @@ class DownloadMutation { ) fun dequeueChapterDownloads( + dataFetchingEnvironment: DataFetchingEnvironment, input: DequeueChapterDownloadsInput, ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input return future { @@ -187,7 +208,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture> { + fun dequeueChapterDownload( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DequeueChapterDownloadInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input return future { @@ -221,9 +246,13 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun startDownloader(input: StartDownloaderInput): CompletableFuture> = + fun startDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: StartDownloaderInput, + ): CompletableFuture> = future { asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.start() StartDownloaderPayload( @@ -249,9 +278,13 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun stopDownloader(input: StopDownloaderInput): CompletableFuture> = + fun stopDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: StopDownloaderInput, + ): CompletableFuture> = future { asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.stop() StopDownloaderPayload( @@ -277,9 +310,13 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun clearDownloader(input: ClearDownloaderInput): CompletableFuture> = + fun clearDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ClearDownloaderInput, + ): CompletableFuture> = future { asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.clear() ClearDownloaderPayload( @@ -307,7 +344,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus, ) - fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture> { + fun reorderChapterDownload( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ReorderChapterDownloadInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter, to) = input return future { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt index d74b7b1d..2a2c66b1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt @@ -2,15 +2,20 @@ package suwayomi.tachidesk.graphql.mutations import eu.kanade.tachiyomi.source.local.LocalSource import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import io.javalin.http.UploadedFile import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.model.table.ExtensionTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class ExtensionMutation { @@ -73,7 +78,11 @@ class ExtensionMutation { } } - fun updateExtension(input: UpdateExtensionInput): CompletableFuture> { + fun updateExtension( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateExtensionInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input return future { @@ -97,7 +106,11 @@ class ExtensionMutation { } } - fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture> { + fun updateExtensions( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateExtensionsInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input return future { @@ -129,7 +142,11 @@ class ExtensionMutation { val extensions: List, ) - fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture> { + fun fetchExtensions( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchExtensionsInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId) = input return future { @@ -163,8 +180,10 @@ class ExtensionMutation { ) fun installExternalExtension( + dataFetchingEnvironment: DataFetchingEnvironment, input: InstallExternalExtensionInput, ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, extensionFile) = input return future { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ImageMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ImageMutation.kt index 50ba1ab4..159fd845 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ImageMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ImageMutation.kt @@ -1,7 +1,12 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.server.ApplicationDirs +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import uy.kohesive.injekt.injectLazy private val applicationDirs: ApplicationDirs by injectLazy() @@ -21,7 +26,11 @@ class ImageMutation { val cachedPages: Boolean?, ) - fun clearCachedImages(input: ClearCachedImagesInput): ClearCachedImagesPayload { + fun clearCachedImages( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ClearCachedImagesInput, + ): ClearCachedImagesPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, downloadedThumbnails, cachedThumbnails, cachedPages) = input val downloadedThumbnailsResult = diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt index 736ce4a9..6101a517 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -1,15 +1,20 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.ERROR import suwayomi.tachidesk.graphql.types.UpdateState.IDLE import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.WebInterfaceManager import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -24,9 +29,13 @@ class InfoMutation { val updateStatus: WebUIUpdateStatus, ) - fun updateWebUI(input: WebUIUpdateInput): CompletableFuture> { + fun updateWebUI( + dataFetchingEnvironment: DataFetchingEnvironment, + input: WebUIUpdateInput, + ): CompletableFuture> { return future { asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() withTimeout(30.seconds) { if (WebInterfaceManager.status.value.state === DOWNLOADING) { return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value) @@ -59,9 +68,10 @@ class InfoMutation { } } - fun resetWebUIUpdateStatus(): CompletableFuture> = + fun resetWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> = future { asDataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() withTimeout(30.seconds) { val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING if (!isUpdateFinished) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt index feafc6aa..bb4da763 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/KoreaderSyncMutation.kt @@ -1,10 +1,15 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class KoreaderSyncMutation { @@ -14,8 +19,12 @@ class KoreaderSyncMutation { val password: String, ) - fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture = + fun connectKoSyncAccount( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ConnectKoSyncAccountInput, + ): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val result = KoreaderSyncService.connect(input.username, input.password) KoSyncConnectPayload( @@ -31,8 +40,12 @@ class KoreaderSyncMutation { val clientMutationId: String? = null, ) - fun logoutKoSyncAccount(input: LogoutKoSyncAccountInput): CompletableFuture = + fun logoutKoSyncAccount( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LogoutKoSyncAccountInput, + ): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() KoreaderSyncService.logout() LogoutKoSyncAccountPayload( clientMutationId = input.clientMutationId, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt index 1e7322a3..f986690d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere @@ -8,6 +9,7 @@ import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.impl.Library @@ -16,7 +18,10 @@ import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import uy.kohesive.injekt.injectLazy import java.time.Instant import java.util.concurrent.CompletableFuture @@ -90,7 +95,11 @@ class MangaMutation { } } - fun updateManga(input: UpdateMangaInput): CompletableFuture> { + fun updateManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangaInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input return future { @@ -110,7 +119,11 @@ class MangaMutation { } } - fun updateMangas(input: UpdateMangasInput): CompletableFuture> { + fun updateMangas( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangasInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input return future { @@ -140,7 +153,11 @@ class MangaMutation { val manga: MangaType, ) - fun fetchManga(input: FetchMangaInput): CompletableFuture> { + fun fetchManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchMangaInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id) = input return future { @@ -169,7 +186,11 @@ class MangaMutation { val meta: MangaMetaType, ) - fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult { + fun setMangaMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetMangaMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input return asDataFetcherResult { @@ -191,7 +212,11 @@ class MangaMutation { val manga: MangaType, ) - fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult { + fun deleteMangaMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteMangaMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId, key) = input return asDataFetcherResult { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt index e83b1cf8..171776f1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.selectAll @@ -8,7 +9,11 @@ import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.GlobalMetaType +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser class MetaMutation { data class SetGlobalMetaInput( @@ -21,7 +26,11 @@ class MetaMutation { val meta: GlobalMetaType, ) - fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult { + fun setGlobalMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetGlobalMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input return asDataFetcherResult { @@ -41,7 +50,11 @@ class MetaMutation { val meta: GlobalMetaType?, ) - fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult { + fun deleteGlobalMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteGlobalMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, key) = input return asDataFetcherResult { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt index 09e5009c..123cf53a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -1,14 +1,19 @@ package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.MutableStateFlow +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.PartialSettingsType import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.requireUser import xyz.nulldev.ts.config.GlobalConfigManager import java.io.File @@ -226,7 +231,11 @@ class SettingsMutation { updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance) } - fun setSettings(input: SetSettingsInput): SetSettingsPayload { + fun setSettings( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetSettingsInput, + ): SetSettingsPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, settings) = input validateSettings(settings) @@ -244,7 +253,11 @@ class SettingsMutation { val settings: SettingsType, ) - fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload { + fun resetSettings( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ResetSettingsInput, + ): ResetSettingsPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId) = input GlobalConfigManager.resetUserConfig() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt index f47d0541..55e8592f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -6,12 +6,14 @@ import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.SwitchPreferenceCompat import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.Preference @@ -25,7 +27,10 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class SourceMutation { @@ -39,7 +44,11 @@ class SourceMutation { val meta: SourceMetaType, ) - fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult { + fun setSourceMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetSourceMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input return asDataFetcherResult { @@ -61,7 +70,11 @@ class SourceMutation { val source: SourceType?, ) - fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult { + fun deleteSourceMeta( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteSourceMetaInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, sourceId, key) = input return asDataFetcherResult { @@ -116,7 +129,11 @@ class SourceMutation { val hasNextPage: Boolean, ) - fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture> { + fun fetchSourceManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchSourceMangaInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, sourceId, type, page, query, filters) = input return future { @@ -182,7 +199,11 @@ class SourceMutation { val source: SourceType, ) - fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult { + fun updateSourcePreference( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateSourcePreferenceInput, + ): DataFetcherResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, sourceId, change) = input return asDataFetcherResult { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/TrackMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/TrackMutation.kt index af1a65cb..92eddda7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/TrackMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/TrackMutation.kt @@ -3,16 +3,21 @@ package suwayomi.tachidesk.graphql.mutations import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDescription import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.TrackRecordType import suwayomi.tachidesk.graphql.types.TrackerType import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager import suwayomi.tachidesk.manga.model.table.TrackRecordTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class TrackMutation { @@ -28,7 +33,11 @@ class TrackMutation { val tracker: TrackerType, ) - fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture { + fun loginTrackerOAuth( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LoginTrackerOAuthInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val tracker = requireNotNull(TrackerManager.getTracker(input.trackerId)) { "Could not find tracker" @@ -57,7 +66,11 @@ class TrackMutation { val tracker: TrackerType, ) - fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture { + fun loginTrackerCredentials( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LoginTrackerCredentialsInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val tracker = requireNotNull(TrackerManager.getTracker(input.trackerId)) { "Could not find tracker" @@ -84,7 +97,11 @@ class TrackMutation { val tracker: TrackerType, ) - fun logoutTracker(input: LogoutTrackerInput): CompletableFuture { + fun logoutTracker( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LogoutTrackerInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val tracker = requireNotNull(TrackerManager.getTracker(input.trackerId)) { "Could not find tracker" @@ -117,7 +134,11 @@ class TrackMutation { val trackRecord: TrackRecordType, ) - fun bindTrack(input: BindTrackInput): CompletableFuture { + fun bindTrack( + dataFetchingEnvironment: DataFetchingEnvironment, + input: BindTrackInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId, trackerId, remoteId, private) = input return future { @@ -152,7 +173,11 @@ class TrackMutation { val trackRecord: TrackRecordType, ) - fun fetchTrack(input: FetchTrackInput): CompletableFuture { + fun fetchTrack( + dataFetchingEnvironment: DataFetchingEnvironment, + input: FetchTrackInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, recordId) = input return future { @@ -184,7 +209,11 @@ class TrackMutation { val trackRecord: TrackRecordType?, ) - fun unbindTrack(input: UnbindTrackInput): CompletableFuture { + fun unbindTrack( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UnbindTrackInput, + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, recordId, deleteRemoteTrack) = input return future { @@ -214,7 +243,11 @@ class TrackMutation { val trackRecords: List, ) - fun trackProgress(input: TrackProgressInput): CompletableFuture> { + fun trackProgress( + dataFetchingEnvironment: DataFetchingEnvironment, + input: TrackProgressInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId) = input return future { @@ -256,8 +289,12 @@ class TrackMutation { val trackRecord: TrackRecordType?, ) - fun updateTrack(input: UpdateTrackInput): CompletableFuture = + fun updateTrack( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateTrackInput, + ): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() Track.update( Track.UpdateInput( input.recordId, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt index a9217221..8cc5dab7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt @@ -1,14 +1,19 @@ package suwayomi.tachidesk.graphql.mutations import graphql.execution.DataFetcherResult +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.asDataFetcherResult +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import uy.kohesive.injekt.injectLazy import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -26,7 +31,11 @@ class UpdateMutation { val updateStatus: LibraryUpdateStatus, ) - fun updateLibrary(input: UpdateLibraryInput): CompletableFuture> { + fun updateLibrary( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateLibraryInput, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() updater.addCategoriesToUpdateQueue( Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true }, clear = true, @@ -57,8 +66,12 @@ class UpdateMutation { val updateStatus: UpdateStatus, ) - fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture> { + fun updateLibraryManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateLibraryMangaInput, + ): CompletableFuture> { updateLibrary( + dataFetchingEnvironment, UpdateLibraryInput( clientMutationId = input.clientMutationId, categories = null, @@ -88,8 +101,12 @@ class UpdateMutation { val updateStatus: UpdateStatus, ) - fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture> { + fun updateCategoryManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryMangaInput, + ): CompletableFuture> { updateLibrary( + dataFetchingEnvironment, UpdateLibraryInput( clientMutationId = input.clientMutationId, categories = input.categories, @@ -117,7 +134,11 @@ class UpdateMutation { val clientMutationId: String?, ) - fun updateStop(input: UpdateStopInput): UpdateStopPayload { + fun updateStop( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateStopInput, + ): UpdateStopPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() updater.reset() return UpdateStopPayload(input.clientMutationId) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt new file mode 100644 index 00000000..714bd070 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt @@ -0,0 +1,64 @@ +package suwayomi.tachidesk.graphql.mutations + +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.global.impl.util.Jwt +import suwayomi.tachidesk.graphql.server.getAttribute +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.UserType + +class UserMutation { + data class LoginInput( + val clientMutationId: String? = null, + val username: String, + val password: String, + ) + + data class LoginPayload( + val clientMutationId: String?, + val accessToken: String, + val refreshToken: String, + ) + + fun login( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LoginInput, + ): LoginPayload { + if (dataFetchingEnvironment.getAttribute(Attribute.TachideskUser) !is UserType.Visitor) { + throw IllegalArgumentException("Cannot login while already logged-in") + } + val isValid = + input.username == serverConfig.authUsername.value && + input.password == serverConfig.authPassword.value + if (isValid) { + val jwt = Jwt.generateJwt() + return LoginPayload( + clientMutationId = input.clientMutationId, + accessToken = jwt.accessToken, + refreshToken = jwt.refreshToken, + ) + } else { + throw Exception("Incorrect username or password.") + } + } + + data class RefreshTokenInput( + val clientMutationId: String? = null, + val refreshToken: String, + ) + + data class RefreshTokenPayload( + val clientMutationId: String?, + val accessToken: String, + ) + + fun refreshToken(input: RefreshTokenInput): RefreshTokenPayload { + val accessToken = Jwt.refreshJwt(input.refreshToken) + + return RefreshTokenPayload( + clientMutationId = input.clientMutationId, + accessToken = accessToken, + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/BackupQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/BackupQuery.kt index ba286c00..6beba8e0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/BackupQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/BackupQuery.kt @@ -1,10 +1,15 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment import io.javalin.http.UploadedFile +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.BackupRestoreStatus import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser class BackupQuery { data class ValidateBackupInput( @@ -25,7 +30,11 @@ class BackupQuery { val missingTrackers: List, ) - fun validateBackup(input: ValidateBackupInput): ValidateBackupResult { + fun validateBackup( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ValidateBackupInput, + ): ValidateBackupResult { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val result = ProtoBackupValidator.validate(input.backup.content()) return ValidateBackupResult( result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) }, @@ -33,5 +42,11 @@ class BackupQuery { ) } - fun restoreStatus(id: String): BackupRestoreStatus? = ProtoBackupImport.getRestoreState(id)?.toStatus() + fun restoreStatus( + dataFetchingEnvironment: DataFetchingEnvironment, + id: String, + ): BackupRestoreStatus? { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return ProtoBackupImport.getRestoreState(id)?.toStatus() + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt index 46526bf4..2570a922 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.CategoryNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class CategoryQuery { fun category( dataFetchingEnvironment: DataFetchingEnvironment, id: Int, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) + } enum class CategoryOrderBy( override val column: Column<*>, @@ -121,6 +128,7 @@ class CategoryQuery { } fun categories( + dataFetchingEnvironment: DataFetchingEnvironment, condition: CategoryCondition? = null, filter: CategoryFilter? = null, @GraphQLDeprecated( @@ -140,6 +148,7 @@ class CategoryQuery { last: Int? = null, offset: Int? = null, ): CategoryNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = CategoryTable.selectAll() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt index 9f5776ac..da3cc2cb 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -30,6 +30,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -43,6 +44,9 @@ import suwayomi.tachidesk.graphql.types.ChapterNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture /** @@ -197,6 +201,7 @@ class ChapterQuery { } fun chapters( + dataFetchingEnvironment: DataFetchingEnvironment, condition: ChapterCondition? = null, filter: ChapterFilter? = null, @GraphQLDeprecated( @@ -216,6 +221,7 @@ class ChapterQuery { last: Int? = null, offset: Int? = null, ): ChapterNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = ChapterTable.selectAll() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/DownloadQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/DownloadQuery.kt index 8722ef6a..ffc209ff 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/DownloadQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/DownloadQuery.kt @@ -1,13 +1,19 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.manga.impl.download.DownloadManager +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class DownloadQuery { - fun downloadStatus(): CompletableFuture = + fun downloadStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() DownloadStatus(DownloadManager.getStatus()) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt index 10f855ee..cdce31ad 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ExtensionQuery.kt @@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.StringFilter import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -40,13 +41,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.ExtensionNodeList import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.manga.model.table.ExtensionTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class ExtensionQuery { fun extension( dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) + } enum class ExtensionOrderBy( override val column: Column<*>, @@ -153,6 +160,7 @@ class ExtensionQuery { } fun extensions( + dataFetchingEnvironment: DataFetchingEnvironment, condition: ExtensionCondition? = null, filter: ExtensionFilter? = null, @GraphQLDeprecated( @@ -172,6 +180,7 @@ class ExtensionQuery { last: Int? = null, offset: Int? = null, ): ExtensionNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = ExtensionTable.selectAll() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt index 11c3a4f3..0b91d02a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -1,14 +1,19 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated +import graphql.schema.DataFetchingEnvironment import suwayomi.tachidesk.global.impl.AppUpdate +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.AboutWebUI import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.WebInterfaceManager import java.util.concurrent.CompletableFuture @@ -42,8 +47,9 @@ class InfoQuery { val url: String, ) - fun checkForServerUpdates(): CompletableFuture> = + fun checkForServerUpdates(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() AppUpdate.checkUpdate().map { CheckForServerUpdatesPayload( channel = it.channel, @@ -53,13 +59,15 @@ class InfoQuery { } } - fun aboutWebUI(): CompletableFuture = + fun aboutWebUI(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() WebInterfaceManager.getAboutInfo() } - fun checkForWebUIUpdate(): CompletableFuture = + fun checkForWebUIUpdate(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true) WebUIUpdateCheck( channel = serverConfig.webUIChannel.value, @@ -68,5 +76,8 @@ class InfoQuery { ) } - fun getWebUIUpdateStatus(): WebUIUpdateStatus = WebInterfaceManager.status.value + fun getWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): WebUIUpdateStatus { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return WebInterfaceManager.status.value + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/KoreaderSyncQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/KoreaderSyncQuery.kt index dd20e406..33d4639e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/KoreaderSyncQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/KoreaderSyncQuery.kt @@ -1,13 +1,19 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class KoreaderSyncQuery { - fun koSyncStatus(): CompletableFuture = + fun koSyncStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() KoreaderSyncService.getStatus() } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index d823f90d..a435da5b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -42,13 +43,19 @@ import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class MangaQuery { fun manga( dataFetchingEnvironment: DataFetchingEnvironment, id: Int, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) + } enum class MangaOrderBy( override val column: Column<*>, @@ -216,6 +223,7 @@ class MangaQuery { } fun mangas( + dataFetchingEnvironment: DataFetchingEnvironment, condition: MangaCondition? = null, filter: MangaFilter? = null, @GraphQLDeprecated( @@ -235,6 +243,7 @@ class MangaQuery { last: Int? = null, offset: Int? = null, ): MangaNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt index a00b3a5a..c2451b6f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -24,6 +24,7 @@ import suwayomi.tachidesk.graphql.queries.filter.OpAnd import suwayomi.tachidesk.graphql.queries.filter.StringFilter import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -35,13 +36,19 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.GlobalMetaNodeList import suwayomi.tachidesk.graphql.types.GlobalMetaType +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class MetaQuery { fun meta( dataFetchingEnvironment: DataFetchingEnvironment, key: String, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) + } enum class MetaOrderBy( override val column: Column<*>, @@ -105,6 +112,7 @@ class MetaQuery { } fun metas( + dataFetchingEnvironment: DataFetchingEnvironment, condition: MetaCondition? = null, filter: MetaFilter? = null, @GraphQLDeprecated( @@ -124,6 +132,7 @@ class MetaQuery { last: Int? = null, offset: Int? = null, ): GlobalMetaNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = GlobalMetaTable.selectAll() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt index 6d9481d0..51d6d697 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt @@ -1,7 +1,15 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.SettingsType +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser class SettingsQuery { - fun settings(): SettingsType = SettingsType() + fun settings(dataFetchingEnvironment: DataFetchingEnvironment): SettingsType { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return SettingsType() + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt index 8c0667fc..8a3ce6cf 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SourceQuery.kt @@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.types.SourceNodeList import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.manga.model.table.SourceTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class SourceQuery { fun source( dataFetchingEnvironment: DataFetchingEnvironment, id: Long, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) + } enum class SourceOrderBy( override val column: Column<*>, @@ -121,6 +128,7 @@ class SourceQuery { } fun sources( + dataFetchingEnvironment: DataFetchingEnvironment, condition: SourceCondition? = null, filter: SourceFilter? = null, @GraphQLDeprecated( @@ -140,6 +148,7 @@ class SourceQuery { last: Int? = null, offset: Int? = null, ): SourceNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (queryResults, resultsAsType) = transaction { val res = SourceTable.selectAll() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/TrackQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/TrackQuery.kt index 6a23da65..a3d9acee 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/TrackQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/TrackQuery.kt @@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.applyOps +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.OrderBy @@ -39,14 +40,20 @@ import suwayomi.tachidesk.graphql.types.TrackerType import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager import suwayomi.tachidesk.manga.model.table.TrackRecordTable import suwayomi.tachidesk.manga.model.table.insertAll +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class TrackQuery { fun tracker( dataFetchingEnvironment: DataFetchingEnvironment, id: Int, - ): CompletableFuture = dataFetchingEnvironment.getValueFromDataLoader("TrackerDataLoader", id) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("TrackerDataLoader", id) + } enum class TrackerOrderBy { ID, @@ -115,6 +122,7 @@ class TrackQuery { ) fun trackers( + dataFetchingEnvironment: DataFetchingEnvironment, condition: TrackerCondition? = null, @GraphQLDeprecated( "Replaced with order", @@ -133,6 +141,7 @@ class TrackQuery { last: Int? = null, offset: Int? = null, ): TrackerNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (queryResults, resultsAsType) = run { var res = TrackerManager.services.map { TrackerType(it) } @@ -240,8 +249,10 @@ class TrackQuery { fun trackRecord( dataFetchingEnvironment: DataFetchingEnvironment, id: Int, - ): CompletableFuture = - dataFetchingEnvironment.getValueFromDataLoader("TrackRecordDataLoader", id) + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return dataFetchingEnvironment.getValueFromDataLoader("TrackRecordDataLoader", id) + } enum class TrackRecordOrderBy( override val column: Column<*>, @@ -389,6 +400,7 @@ class TrackQuery { } fun trackRecords( + dataFetchingEnvironment: DataFetchingEnvironment, condition: TrackRecordCondition? = null, filter: TrackRecordFilter? = null, @GraphQLDeprecated( @@ -408,6 +420,7 @@ class TrackQuery { last: Int? = null, offset: Int? = null, ): TrackRecordNodeList { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { val res = TrackRecordTable.selectAll() @@ -490,8 +503,12 @@ class TrackQuery { val trackSearches: List, ) - fun searchTracker(input: SearchTrackerInput): CompletableFuture = + fun searchTracker( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SearchTrackerInput, + ): CompletableFuture = future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val tracker = requireNotNull(TrackerManager.getTracker(input.trackerId)) { "Tracker not found" diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt index 94c0a67e..4e349d5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt @@ -1,11 +1,16 @@ package suwayomi.tachidesk.graphql.queries import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import uy.kohesive.injekt.injectLazy import java.util.concurrent.CompletableFuture @@ -13,13 +18,24 @@ class UpdateQuery { private val updater: IUpdater by injectLazy() @GraphQLDeprecated("Replaced with libraryUpdateStatus", ReplaceWith("libraryUpdateStatus")) - fun updateStatus(): CompletableFuture = future { UpdateStatus(updater.status.first()) } + fun updateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = + future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + UpdateStatus(updater.status.first()) + } - fun libraryUpdateStatus(): CompletableFuture = future { LibraryUpdateStatus(updater.getStatus()) } + fun libraryUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture = + future { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + LibraryUpdateStatus(updater.getStatus()) + } data class LastUpdateTimestampPayload( val timestamp: Long, ) - fun lastUpdateTimestamp(): LastUpdateTimestampPayload = LastUpdateTimestampPayload(updater.getLastUpdateTimestamp()) + fun lastUpdateTimestamp(dataFetchingEnvironment: DataFetchingEnvironment): LastUpdateTimestampPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return LastUpdateTimestampPayload(updater.getLastUpdateTimestamp()) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt index bb7c2d91..13932689 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt @@ -9,34 +9,49 @@ package suwayomi.tachidesk.graphql.server import com.expediagroup.graphql.server.execution.GraphQLContextFactory import graphql.GraphQLContext +import graphql.schema.DataFetchingEnvironment import io.javalin.http.Context import io.javalin.websocket.WsContext +import org.dataloader.BatchLoaderEnvironment +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.UserType /** * Custom logic for how Suwayomi-Server should create its context given the [Context] */ class TachideskGraphQLContextFactory : GraphQLContextFactory { - override suspend fun generateContext(request: Context): GraphQLContext = emptyMap().toGraphQLContext() -// mutableMapOf( -// "user" to User( -// email = "fake@site.com", -// firstName = "Someone", -// lastName = "You Don't know", -// universityId = 4 -// ) -// ).also { map -> -// request.headers["my-custom-header"]?.let { customHeader -> -// map["customHeader"] = customHeader -// } -// }.toGraphQLContext() + override suspend fun generateContext(request: Context): GraphQLContext = + mapOf( + Context::class to request, + request.getPair(Attribute.TachideskUser), + ).toGraphQLContext() fun generateContextMap( - @Suppress("UNUSED_PARAMETER") request: WsContext, - ): Map<*, Any> = emptyMap() + user: UserType, + request: WsContext, + ): Map<*, Any> = + mapOf( + Context::class to request, + Attribute.TachideskUser to user, + ) + + private fun Context.getPair(attribute: Attribute) = attribute to getAttribute(attribute) + + private fun WsContext.getPair(attribute: Attribute) = attribute to getAttribute(attribute) } /** * Create a [GraphQLContext] from [this] map * @return a new [GraphQLContext] */ -fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = graphql.GraphQLContext.of(this) +fun Map<*, Any?>.toGraphQLContext(): GraphQLContext = GraphQLContext.of(this) + +fun GraphQLContext.getAttribute(attribute: Attribute): T = get(attribute) + +fun DataFetchingEnvironment.getAttribute(attribute: Attribute): T = graphQlContext.get(attribute) + +val BatchLoaderEnvironment.graphQlContext: GraphQLContext + get() = keyContextsList.filterIsInstance().first() + +fun BatchLoaderEnvironment.getAttribute(attribute: Attribute): T = graphQlContext.getAttribute(attribute) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index 9af55a9b..c9231c5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.mutations.SettingsMutation import suwayomi.tachidesk.graphql.mutations.SourceMutation import suwayomi.tachidesk.graphql.mutations.TrackMutation import suwayomi.tachidesk.graphql.mutations.UpdateMutation +import suwayomi.tachidesk.graphql.mutations.UserMutation import suwayomi.tachidesk.graphql.queries.BackupQuery import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery @@ -100,6 +101,7 @@ val schema = TopLevelObject(SourceMutation()), TopLevelObject(TrackMutation()), TopLevelObject(UpdateMutation()), + TopLevelObject(UserMutation()), ), subscriptions = listOf( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt index 8af2d722..5cb20dd9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.readValue import io.github.oshai.kotlinlogging.KotlinLogging +import io.javalin.http.Header import io.javalin.websocket.WsContext import io.javalin.websocket.WsMessageContext import kotlinx.coroutines.currentCoroutineContext @@ -36,6 +37,8 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_NEXT import suwayomi.tachidesk.graphql.server.toGraphQLContext +import suwayomi.tachidesk.server.user.UserType +import suwayomi.tachidesk.server.user.getUserFromToken /** * Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina @@ -77,7 +80,7 @@ class ApolloSubscriptionProtocolHandler( return try { when (operationMessage.type) { - GQL_CONNECTION_INIT.type -> onInit(context) + GQL_CONNECTION_INIT.type -> onInit(operationMessage, context) GQL_SUBSCRIBE.type -> startSubscription(operationMessage, context) GQL_COMPLETE.type -> onComplete(operationMessage) GQL_PING.type -> onPing() @@ -144,17 +147,27 @@ class ApolloSubscriptionProtocolHandler( } } - private fun onInit(context: WsContext): Flow { - saveContext(context) + private fun onInit( + operationMessage: SubscriptionOperationMessage, + context: WsContext, + ): Flow { + @Suppress("UNCHECKED_CAST") + val payload = operationMessage.payload as? Map + val token = payload?.let { it[Header.AUTHORIZATION] as? String } + + saveContext(getUserFromToken(token), context) return flowOf(acknowledgeMessage) } /** * Generate the context and save it for all future messages. */ - private fun saveContext(context: WsContext) { + private fun saveContext( + user: UserType, + context: WsContext, + ) { runBlocking { - val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext() + val graphQLContext = contextFactory.generateContextMap(user, context).toGraphQLContext() sessionState.saveContext(context, graphQLContext) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt index 9cfd2b97..2d55b7c1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/DownloadSubscription.kt @@ -9,18 +9,25 @@ package suwayomi.tachidesk.graphql.subscriptions import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadUpdates import suwayomi.tachidesk.manga.impl.download.DownloadManager +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser class DownloadSubscription { @GraphQLDeprecated("Replaced with downloadStatusChanged", ReplaceWith("downloadStatusChanged(input)")) - fun downloadChanged(): Flow = - DownloadManager.status.map { downloadStatus -> + fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return DownloadManager.status.map { downloadStatus -> DownloadStatus(downloadStatus) } + } data class DownloadChangedInput( @GraphQLDescription( @@ -33,7 +40,11 @@ class DownloadSubscription { val maxUpdates: Int?, ) - fun downloadStatusChanged(input: DownloadChangedInput): Flow { + fun downloadStatusChanged( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DownloadChangedInput, + ): Flow { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val omitUpdates = input.maxUpdates != null val maxUpdates = input.maxUpdates ?: 50 diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt index a0058fad..35a54100 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/InfoSubscription.kt @@ -1,9 +1,17 @@ package suwayomi.tachidesk.graphql.subscriptions +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.Flow +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.WebInterfaceManager class InfoSubscription { - fun webUIUpdateStatusChange(): Flow = WebInterfaceManager.status + fun webUIUpdateStatusChange(dataFetchingEnvironment: DataFetchingEnvironment): Flow { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return WebInterfaceManager.status + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt index a29d08b0..280e1eb6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt @@ -9,22 +9,29 @@ package suwayomi.tachidesk.graphql.subscriptions import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.graphql.types.UpdaterUpdates import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.UpdateUpdates +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import uy.kohesive.injekt.injectLazy class UpdateSubscription { private val updater: IUpdater by injectLazy() @GraphQLDeprecated("Replaced with updates", ReplaceWith("updates(input)")) - fun updateStatusChanged(): Flow = - updater.status.map { updateStatus -> + fun updateStatusChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return updater.status.map { updateStatus -> UpdateStatus(updateStatus) } + } data class LibraryUpdateStatusChangedInput( @GraphQLDescription( @@ -37,7 +44,11 @@ class UpdateSubscription { val maxUpdates: Int?, ) - fun libraryUpdateStatusChanged(input: LibraryUpdateStatusChangedInput): Flow { + fun libraryUpdateStatusChanged( + dataFetchingEnvironment: DataFetchingEnvironment, + input: LibraryUpdateStatusChangedInput, + ): Flow { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val omitUpdates = input.maxUpdates != null val maxUpdates = input.maxUpdates ?: 50 diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/AuthMode.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/AuthMode.kt index 2a6f9733..9fa104b1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/AuthMode.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/AuthMode.kt @@ -4,6 +4,7 @@ enum class AuthMode { NONE, BASIC_AUTH, SIMPLE_LOGIN, + UI_LOGIN, // TODO: ACCOUNT for #623 ; diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt index 01f87660..3be3f8e8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt @@ -6,7 +6,10 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.withOperation @@ -28,6 +31,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { ProtoBackupImport.restoreLegacy(ctx.bodyInputStream()) @@ -55,6 +59,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" ctx.future { future { @@ -80,6 +85,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.contentType("application/octet-stream") ctx.future { future { @@ -112,6 +118,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.contentType("application/octet-stream") ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""") @@ -146,6 +153,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { ProtoBackupValidator.validate(ctx.bodyInputStream()) @@ -177,6 +185,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt index 0ea20c1f..98db3b8d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt @@ -12,6 +12,9 @@ import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam @@ -28,6 +31,7 @@ object CategoryController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Category.getCategoryList()) }, withResults = { @@ -46,6 +50,7 @@ object CategoryController { } }, behaviorOf = { ctx, name -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() if (Category.createCategory(name) != -1) { ctx.status(200) } else { @@ -73,6 +78,7 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate, includeInDownload -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Category.updateCategory(categoryId, name, isDefault, includeInUpdate, includeInDownload) ctx.status(200) }, @@ -92,6 +98,7 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Category.removeCategory(categoryId) ctx.status(200) }, @@ -111,6 +118,7 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(CategoryManga.getCategoryMangaList(categoryId)) }, withResults = { @@ -130,6 +138,7 @@ object CategoryController { } }, behaviorOf = { ctx, from, to -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Category.reorderCategory(from, to) ctx.status(200) }, @@ -151,6 +160,7 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId, key, value -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Category.modifyMeta(categoryId, key, value) ctx.status(200) }, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt index 56cc6e04..8515378b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt @@ -12,7 +12,10 @@ import io.javalin.websocket.WsConfig import kotlinx.serialization.json.Json import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.withOperation @@ -24,6 +27,7 @@ object DownloadController { /** Download queue stats */ fun downloadsWS(ws: WsConfig) { ws.onConnect { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.addClient(ctx) DownloadManager.notifyClient(ctx) } @@ -44,7 +48,8 @@ object DownloadController { description("Start the downloader") } }, - behaviorOf = { + behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.start() }, withResults = { @@ -62,6 +67,7 @@ object DownloadController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { DownloadManager.stop() } .thenApply { ctx.status(HttpStatus.OK) } @@ -82,6 +88,7 @@ object DownloadController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { DownloadManager.clear() } .thenApply { ctx.status(HttpStatus.OK) } @@ -104,6 +111,7 @@ object DownloadController { } }, behaviorOf = { ctx, chapterIndex, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex) @@ -126,6 +134,7 @@ object DownloadController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val inputs = json.decodeFromString(ctx.body()) ctx.future { future { @@ -149,6 +158,7 @@ object DownloadController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) ctx.future { future { @@ -173,6 +183,7 @@ object DownloadController { } }, behaviorOf = { ctx, chapterIndex, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.dequeue(chapterIndex, mangaId) ctx.status(200) @@ -194,7 +205,8 @@ object DownloadController { description("Reorder chapter in download queue") } }, - behaviorOf = { _, chapterIndex, mangaId, to -> + behaviorOf = { ctx, chapterIndex, mangaId, to -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.reorder(chapterIndex, mangaId, to) }, withResults = { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt index ebea9427..006b27ad 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt @@ -12,7 +12,10 @@ import io.javalin.http.HttpStatus import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.withOperation @@ -31,6 +34,7 @@ object ExtensionController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { ExtensionsList.getExtensionList() @@ -55,6 +59,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Extension.installExtension(pkgName) @@ -84,6 +89,7 @@ object ExtensionController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val uploadedFile = ctx.uploadedFile("file")!! logger.debug { "Uploaded extension file name: " + uploadedFile.filename() } @@ -116,6 +122,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Extension.updateExtension(pkgName) @@ -143,6 +150,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Extension.uninstallExtension(pkgName) ctx.status(200) }, @@ -165,6 +173,7 @@ object ExtensionController { } }, behaviorOf = { ctx, apkName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Extension.getExtensionIcon(apkName) } .thenApply { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt index 67e445f1..b84d619b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -26,7 +26,10 @@ import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam @@ -49,6 +52,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Manga.getManga(mangaId, onlineFetch) @@ -73,6 +77,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Manga.getMangaFull(mangaId, onlineFetch) @@ -96,6 +101,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Manga.getMangaThumbnail(mangaId) } .thenApply { @@ -123,6 +129,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Library.addMangaToLibrary(mangaId) } .thenApply { ctx.status(HttpStatus.OK) } @@ -145,6 +152,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Library.removeMangaFromLibrary(mangaId) } .thenApply { ctx.status(HttpStatus.OK) } @@ -167,6 +175,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(CategoryManga.getMangaCategories(mangaId)) }, withResults = { @@ -186,6 +195,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() CategoryManga.addMangaToCategory(mangaId, categoryId) ctx.status(200) }, @@ -206,6 +216,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() CategoryManga.removeMangaFromCategory(mangaId, categoryId) ctx.status(200) }, @@ -227,6 +238,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, key, value -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Manga.modifyMangaMeta(mangaId, key, value) ctx.status(200) }, @@ -252,6 +264,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Chapter.getChapterList(mangaId, onlineFetch) } .thenApply { ctx.json(it) } @@ -275,6 +288,7 @@ object MangaController { body() }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) Chapter.modifyChapters(input, mangaId) }, @@ -294,6 +308,7 @@ object MangaController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) Chapter.modifyChapters( Chapter.MangaChapterBatchEditInput( @@ -320,6 +335,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { var chapter = getChapterDownloadReadyByIndex(chapterIndex, mangaId) @@ -368,6 +384,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val chapterId = Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) // Sync with KoreaderSync when progress is updated @@ -394,6 +411,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Chapter.deleteChapter(mangaId, chapterIndex) ctx.status(200) @@ -418,6 +436,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, key, value -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) ctx.status(200) @@ -445,6 +464,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress, format -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Page.getPageImage(mangaId, chapterIndex, index, format, null) } .thenApply { @@ -480,6 +500,7 @@ object MangaController { } }, behaviorOf = { ctx, chapterId, markAsRead -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() if (ctx.method() == HandlerType.HEAD) { ctx.future { future { ChapterDownloadHelper.getCbzMetadataForDownload(chapterId) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt index d67be69c..569eb91f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt @@ -17,7 +17,10 @@ import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.queryParam @@ -35,6 +38,7 @@ object SourceController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSourceList()) }, withResults = { @@ -53,6 +57,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSource(sourceId)!!) }, withResults = { @@ -73,6 +78,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, pageNum -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { MangaList.getMangaList(sourceId, pageNum, popular = true) @@ -96,6 +102,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, pageNum -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { MangaList.getMangaList(sourceId, pageNum, popular = false) @@ -118,6 +125,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSourcePreferences(sourceId)) }, withResults = { @@ -137,6 +145,7 @@ object SourceController { body() }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value)) }, @@ -157,6 +166,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, reset -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Search.getFilterList(sourceId, reset)) }, withResults = { @@ -179,6 +189,7 @@ object SourceController { body>() }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val filterChange = try { json.decodeFromString>(ctx.body()) @@ -206,6 +217,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, searchTerm, pageNum -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Search.sourceSearch(sourceId, searchTerm, pageNum) } .thenApply { ctx.json(it) } @@ -229,6 +241,7 @@ object SourceController { body() }, behaviorOf = { ctx, sourceId, pageNum -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val filter = json.decodeFromString(ctx.body()) ctx.future { future { Search.sourceFilter(sourceId, pageNum, filter) } @@ -251,6 +264,7 @@ object SourceController { } }, behaviorOf = { ctx, searchTerm -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() // TODO ctx.json(Search.sourceGlobalSearch(searchTerm)) }, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/TrackController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/TrackController.kt index c7dcf58e..637d7342 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/TrackController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/TrackController.kt @@ -12,7 +12,10 @@ import io.javalin.http.HttpStatus import kotlinx.serialization.json.Json import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.queryParam @@ -33,6 +36,7 @@ object TrackController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Track.getTrackerList()) }, withResults = { @@ -50,6 +54,7 @@ object TrackController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) logger.debug { "tracker login $input" } ctx.future { @@ -73,6 +78,7 @@ object TrackController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) logger.debug { "tracker logout $input" } ctx.future { @@ -96,6 +102,7 @@ object TrackController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) logger.debug { "tracker search $input" } ctx.future { @@ -122,6 +129,7 @@ object TrackController { } }, behaviorOf = { ctx, mangaId, trackerId, remoteId, private -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Track.bind(mangaId, trackerId, remoteId.toLong(), private) } .thenApply { ctx.status(HttpStatus.OK) } @@ -142,6 +150,7 @@ object TrackController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) logger.debug { "tracker update $input" } ctx.future { @@ -164,6 +173,7 @@ object TrackController { } }, behaviorOf = { ctx, trackerId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Track.getTrackerThumbnail(trackerId) } .thenApply { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt index d9c93fcc..1fe1559a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -10,7 +10,10 @@ import suwayomi.tachidesk.manga.impl.update.UpdateStatus import suwayomi.tachidesk.manga.impl.update.UpdaterSocket import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.PaginatedList +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam @@ -39,6 +42,7 @@ object UpdateController { } }, behaviorOf = { ctx, pageNum -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future { future { Chapter.getRecentChapters(pageNum) @@ -66,6 +70,7 @@ object UpdateController { } }, behaviorOf = { ctx, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater = Injekt.get() if (categoryId == null) { logger.info { "Adding Library to Update Queue" } @@ -96,6 +101,7 @@ object UpdateController { fun categoryUpdateWS(ws: WsConfig) { ws.onConnect { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() UpdaterSocket.addClient(ctx) } ws.onMessage { ctx -> @@ -115,6 +121,7 @@ object UpdateController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater = Injekt.get() ctx.json(updater.statusDeprecated.value) }, @@ -132,6 +139,7 @@ object UpdateController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater = Injekt.get() logger.info { "Resetting Updater" } ctx.future { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/opds/controller/OpdsV1Controller.kt b/server/src/main/kotlin/suwayomi/tachidesk/opds/controller/OpdsV1Controller.kt index 385f3ec1..7c65d804 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/opds/controller/OpdsV1Controller.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/opds/controller/OpdsV1Controller.kt @@ -9,7 +9,10 @@ import suwayomi.tachidesk.opds.dto.OpdsMangaFilter import suwayomi.tachidesk.opds.dto.OpdsSearchCriteria import suwayomi.tachidesk.opds.dto.PrimaryFilterType import suwayomi.tachidesk.opds.impl.OpdsFeedBuilder +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.JavalinSetup.getAttribute +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.queryParam @@ -62,6 +65,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.contentType(OPDS_MIME).result(OpdsFeedBuilder.getRootFeed(BASE_URL, locale)) }, @@ -84,6 +88,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -109,6 +114,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.contentType("application/opensearchdescription+xml").result( """ @@ -136,6 +142,7 @@ object OpdsV1Controller { handler( documentWith = { withOperation { summary("OPDS Series in Library Feed") } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val pageNumber = ctx.queryParam("pageNumber")?.toIntOrNull() val query = ctx.queryParam("query") val author = ctx.queryParam("author") @@ -188,6 +195,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -214,6 +222,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -240,6 +249,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -266,6 +276,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -291,6 +302,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -316,6 +328,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -342,6 +355,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, pageNumber, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -370,6 +384,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, sourceId, pageNumber, sort, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -405,6 +420,7 @@ object OpdsV1Controller { pathParam("sourceId"), documentWith = { withOperation { summary("OPDS Library Source Specific Series Feed") } }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(sourceId = sourceId, primaryFilter = PrimaryFilterType.SOURCE)) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) }, @@ -422,6 +438,7 @@ object OpdsV1Controller { pathParam("categoryId"), documentWith = { withOperation { summary("OPDS Category Specific Series Feed") } }, behaviorOf = { ctx, categoryId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(categoryId = categoryId, primaryFilter = PrimaryFilterType.CATEGORY)) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) @@ -440,6 +457,7 @@ object OpdsV1Controller { pathParam("genre"), documentWith = { withOperation { summary("OPDS Genre Specific Series Feed") } }, behaviorOf = { ctx, genre -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(genre = genre, primaryFilter = PrimaryFilterType.GENRE)) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) }, @@ -457,6 +475,7 @@ object OpdsV1Controller { pathParam("statusId"), documentWith = { withOperation { summary("OPDS Status Specific Series Feed") } }, behaviorOf = { ctx, statusId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(statusId = statusId, primaryFilter = PrimaryFilterType.STATUS)) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) }, @@ -479,6 +498,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, langCode -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(langCode = langCode, primaryFilter = PrimaryFilterType.LANGUAGE)) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) @@ -506,6 +526,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, seriesId, pageNumber, sort, filter, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { @@ -536,6 +557,7 @@ object OpdsV1Controller { } }, behaviorOf = { ctx, seriesId, chapterIndex, lang -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) ctx.future { future { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index f1c60b66..005f7143 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -12,12 +12,14 @@ import gg.jte.TemplateEngine import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.path +import io.javalin.http.Context import io.javalin.http.HandlerType import io.javalin.http.HttpStatus import io.javalin.http.RedirectResponse import io.javalin.http.UnauthorizedResponse import io.javalin.http.staticfiles.Location import io.javalin.rendering.template.JavalinJte +import io.javalin.websocket.WsContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -31,6 +33,11 @@ import suwayomi.tachidesk.graphql.types.AuthMode import suwayomi.tachidesk.i18n.LocalizationHelper import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.opds.OpdsAPI +import suwayomi.tachidesk.server.user.ForbiddenException +import suwayomi.tachidesk.server.user.UnauthorizedException +import suwayomi.tachidesk.server.user.UserType +import suwayomi.tachidesk.server.user.getUserFromContext +import suwayomi.tachidesk.server.user.getUserFromWsContext import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.WebInterfaceManager import uy.kohesive.injekt.injectLazy @@ -207,6 +214,8 @@ object JavalinSetup { ctx.header("WWW-Authenticate", "Basic") throw UnauthorizedResponse() } + + ctx.setAttribute(Attribute.TachideskUser, getUserFromContext(ctx)) } app.events { event -> @@ -217,6 +226,12 @@ object JavalinSetup { } } + app.wsBefore { + it.onConnect { ctx -> + ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx)) + } + } + // when JVM is prompted to shutdown, stop javalin gracefully Runtime.getRuntime().addShutdownHook( thread(start = false) { @@ -244,6 +259,18 @@ object JavalinSetup { ctx.result(e.message ?: "Bad Request") } + app.exception(UnauthorizedException::class.java) { e, ctx -> + logger.error(e) { "UnauthorizedException while handling the request" } + ctx.status(HttpStatus.UNAUTHORIZED) + ctx.result(e.message ?: "Unauthorized") + } + + app.exception(ForbiddenException::class.java) { e, ctx -> + logger.error(e) { "ForbiddenException while handling the request" } + ctx.status(HttpStatus.FORBIDDEN) + ctx.result(e.message ?: "Forbidden") + } + app.start() } @@ -262,4 +289,28 @@ object JavalinSetup { // ) // } // } + + sealed class Attribute( + val name: String, + ) { + data object TachideskUser : Attribute("user") + } + + private fun Context.setAttribute( + attribute: Attribute, + value: T, + ) { + attribute(attribute.name, value) + } + + private fun WsContext.setAttribute( + attribute: Attribute, + value: T, + ) { + attribute(attribute.name, value) + } + + fun Context.getAttribute(attribute: Attribute): T = attribute(attribute.name)!! + + fun WsContext.getAttribute(attribute: Attribute): T = attribute(attribute.name)!! } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index 36a433df..a3c0e9a4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -34,6 +34,7 @@ import suwayomi.tachidesk.graphql.types.WebUIInterface import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import kotlin.reflect.KProperty +import kotlin.time.Duration val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @@ -152,6 +153,9 @@ class ServerConfig( val authMode: MutableStateFlow by OverrideConfigValue() val authUsername: MutableStateFlow by OverrideConfigValue() val authPassword: MutableStateFlow by OverrideConfigValue() + val jwtAudience: MutableStateFlow by OverrideConfigValue() + val jwtTokenExpiry: MutableStateFlow by OverrideConfigValue() + val jwtRefreshExpiry: MutableStateFlow by OverrideConfigValue() val basicAuthEnabled: MutableStateFlow by MigratedConfigValue({ authMode.value == AuthMode.BASIC_AUTH }) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index fbaf5550..bff30e3e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -48,6 +48,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.renameTo import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex +import suwayomi.tachidesk.server.util.DurationType import suwayomi.tachidesk.server.util.MutableStateFlowType import suwayomi.tachidesk.server.util.SystemTray import uy.kohesive.injekt.Injekt @@ -176,6 +177,7 @@ fun applicationSetup() { // register Tachidesk's config which is dubbed "ServerConfig" registerCustomType(MutableStateFlowType()) + registerCustomType(DurationType()) GlobalConfigManager.registerModule( ServerConfig.register { GlobalConfigManager.config }, ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt new file mode 100644 index 00000000..c2b6659d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt @@ -0,0 +1,53 @@ +package suwayomi.tachidesk.server.user + +import io.javalin.http.Context +import io.javalin.http.Header +import io.javalin.websocket.WsConnectContext +import suwayomi.tachidesk.global.impl.util.Jwt +import suwayomi.tachidesk.graphql.types.AuthMode +import suwayomi.tachidesk.server.serverConfig + +sealed class UserType { + class Admin( + val id: Int, + ) : UserType() + + data object Visitor : UserType() +} + +fun UserType.requireUser(): Int = + when (this) { + is UserType.Admin -> id + UserType.Visitor -> throw UnauthorizedException() + } + +fun getUserFromToken(token: String?): UserType { + if (serverConfig.authMode.value != AuthMode.UI_LOGIN) { + return UserType.Admin(1) + } + + if (token.isNullOrBlank()) { + return UserType.Visitor + } + + return Jwt.verifyJwt(token) +} + +fun getUserFromContext(ctx: Context): UserType { + val authentication = ctx.header(Header.AUTHORIZATION) ?: ctx.cookie("suwayomi-server-token") + val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token") + + return getUserFromToken(token) +} + +fun getUserFromWsContext(ctx: WsConnectContext): UserType { + val authentication = + ctx.header(Header.AUTHORIZATION) ?: ctx.header("Sec-WebSocket-Protocol") ?: ctx.cookie("suwayomi-server-token") + val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token") + + return getUserFromToken(token) +} + +class UnauthorizedException : IllegalStateException("Unauthorized") + +class ForbiddenException : IllegalStateException("Forbidden") diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/DurationType.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/DurationType.kt new file mode 100644 index 00000000..d588cde4 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/DurationType.kt @@ -0,0 +1,31 @@ +package suwayomi.tachidesk.server.util + +import com.typesafe.config.Config +import io.github.config4k.ClassContainer +import io.github.config4k.CustomType +import io.github.config4k.readers.SelectReader +import io.github.config4k.toConfig +import kotlin.time.Duration + +class DurationType : CustomType { + override fun parse( + clazz: ClassContainer, + config: Config, + name: String, + ): Any? { + val clazz = ClassContainer(String::class) + val reader = SelectReader.getReader(clazz) + val path = name + val result = reader(config, path) as String + return Duration.parse(result) + } + + override fun testParse(clazz: ClassContainer): Boolean = clazz.mapperClass.qualifiedName == "kotlin.time.Duration" + + override fun testToConfig(obj: Any): Boolean = obj as? Duration != null + + override fun toConfig( + obj: Any, + name: String, + ): Config = (obj as Duration).toString().toConfig(name) +} diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index ec1018b5..4f1f3d9f 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update # Authentication -server.authMode = "none" # none, basic_auth or simple_login +server.authMode = "none" # none, basic_auth, simple_login or ui_login server.authUsername = "" server.authPassword = "" +server.jwtAudience = "suwayomi-server-api" +server.jwtTokenExpiry = "5m" +server.jwtRefreshExpiry = "60d" # misc server.debugLogsEnabled = false diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf index 96f7420c..c5904e27 100644 --- a/server/src/test/resources/server-reference.conf +++ b/server/src/test/resources/server-reference.conf @@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update # Authentication -server.authMode = "none" # none, basic_auth or simple_login +server.authMode = "none" # none, basic_auth, simple_login or ui_login server.authUsername = "" server.authPassword = "" +server.jwtAudience = "suwayomi-server-api" +server.jwtTokenExpiry = "5m" +server.jwtRefreshExpiry = "60d" # misc server.debugLogsEnabled = false