From 1991a82da32342a4eaa67b5d530ed6aeabdf452a Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 29 Jul 2023 19:22:53 -0400 Subject: [PATCH 01/18] Initial user data seperation --- build.gradle.kts | 6 +- server/build.gradle.kts | 6 +- .../global/controller/GlobalMetaController.kt | 9 +- .../global/controller/SettingsController.kt | 5 + .../tachidesk/global/impl/GlobalMeta.kt | 13 +- .../global/model/table/GlobalMetaTable.kt | 2 + .../graphql/dataLoaders/CategoryDataLoader.kt | 15 +- .../graphql/dataLoaders/ChapterDataLoader.kt | 17 +- .../dataLoaders/ExtensionDataLoader.kt | 5 +- .../graphql/dataLoaders/MangaDataLoader.kt | 28 ++- .../graphql/dataLoaders/MetaDataLoader.kt | 9 +- .../graphql/dataLoaders/SourceDataLoader.kt | 5 +- .../graphql/mutations/BackupMutation.kt | 11 +- .../graphql/mutations/CategoryMutation.kt | 125 +++++++++---- .../graphql/mutations/ChapterMutation.kt | 61 ++++-- .../graphql/mutations/ExtensionMutation.kt | 18 +- .../graphql/mutations/MangaMutation.kt | 72 +++++-- .../graphql/mutations/MetaMutation.kt | 15 +- .../graphql/mutations/SourceMutation.kt | 11 +- .../tachidesk/graphql/queries/BackupQuery.kt | 9 +- .../graphql/queries/CategoryQuery.kt | 10 +- .../tachidesk/graphql/queries/ChapterQuery.kt | 39 ++-- .../graphql/queries/ExtensionQuery.kt | 6 + .../tachidesk/graphql/queries/MangaQuery.kt | 28 ++- .../tachidesk/graphql/queries/MetaQuery.kt | 10 +- .../tachidesk/graphql/queries/SourceQuery.kt | 7 + .../TachideskDataFetcherExceptionHandler.kt | 26 +++ .../server/TachideskGraphQLContextFactory.kt | 43 ++--- .../graphql/server/TachideskGraphQLServer.kt | 1 + .../ApolloSubscriptionProtocolHandler.kt | 2 +- .../ApolloSubscriptionSessionState.kt | 2 +- .../GraphQLSubscriptionHandler.kt | 4 +- .../tachidesk/graphql/types/ChapterType.kt | 9 +- .../tachidesk/graphql/types/MangaType.kt | 5 +- .../manga/controller/BackupController.kt | 15 +- .../manga/controller/CategoryController.kt | 24 ++- .../manga/controller/DownloadController.kt | 15 +- .../manga/controller/ExtensionController.kt | 9 + .../manga/controller/MangaController.kt | 47 +++-- .../manga/controller/SourceController.kt | 21 ++- .../manga/controller/UpdateController.kt | 13 +- .../suwayomi/tachidesk/manga/impl/Category.kt | 84 +++++---- .../tachidesk/manga/impl/CategoryManga.kt | 61 +++--- .../suwayomi/tachidesk/manga/impl/Chapter.kt | 176 +++++++++++++----- .../suwayomi/tachidesk/manga/impl/Library.kt | 37 ++-- .../suwayomi/tachidesk/manga/impl/Manga.kt | 65 ++++--- .../tachidesk/manga/impl/MangaList.kt | 18 +- .../suwayomi/tachidesk/manga/impl/Search.kt | 8 +- .../manga/impl/backup/models/ChapterImpl.kt | 16 -- .../impl/backup/proto/ProtoBackupExport.kt | 26 +-- .../impl/backup/proto/ProtoBackupImport.kt | 104 ++++++++--- .../manga/impl/chapter/ChapterForDownload.kt | 10 +- .../manga/impl/download/DownloadManager.kt | 5 +- .../manga/impl/download/Downloader.kt | 2 +- .../tachidesk/manga/impl/update/Updater.kt | 8 +- .../manga/model/table/CategoryMangaTable.kt | 2 + .../manga/model/table/CategoryMetaTable.kt | 2 + .../manga/model/table/CategoryTable.kt | 7 +- .../manga/model/table/ChapterMetaTable.kt | 2 + .../manga/model/table/ChapterTable.kt | 16 +- .../manga/model/table/MangaMetaTable.kt | 2 + .../tachidesk/manga/model/table/MangaTable.kt | 11 +- .../suwayomi/tachidesk/server/JavalinSetup.kt | 32 ++++ .../controller/CategoryControllerTest.kt | 10 +- .../manga/controller/UpdateControllerTest.kt | 19 +- .../tachidesk/manga/impl/CategoryMangaTest.kt | 20 +- .../tachidesk/manga/impl/MangaTest.kt | 20 +- .../tachidesk/manga/impl/SearchTest.kt | 18 +- .../suwayomi/tachidesk/test/TestUtils.kt | 22 ++- 69 files changed, 1087 insertions(+), 494 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt diff --git a/build.gradle.kts b/build.gradle.kts index 93a920ea4..d27aa5e57 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,8 +27,8 @@ allprojects { subprojects { plugins.withType { extensions.configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } } @@ -36,7 +36,7 @@ subprojects { withType { dependsOn("formatKotlin") kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } } diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 883160803..8389da2d8 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -65,9 +65,9 @@ dependencies { // implementation(fileTree("lib/")) implementation(kotlin("script-runtime")) - implementation("com.expediagroup:graphql-kotlin-server:6.4.0") - implementation("com.expediagroup:graphql-kotlin-schema-generator:6.4.0") - implementation("com.graphql-java:graphql-java-extended-scalars:20.0") + implementation("com.expediagroup:graphql-kotlin-server:7.0.0-alpha.6") + implementation("com.expediagroup:graphql-kotlin-schema-generator:7.0.0-alpha.6") + implementation("com.graphql-java:graphql-java-extended-scalars:20.2") testImplementation(libs.mockk) 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 178a8ed39..dbac06a0b 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.HttpCode 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 @@ -23,7 +26,8 @@ object GlobalMetaController { } }, behaviorOf = { ctx -> - ctx.json(GlobalMeta.getMetaMap()) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.json(GlobalMeta.getMetaMap(userId)) ctx.status(200) }, withResults = { @@ -42,7 +46,8 @@ object GlobalMetaController { } }, behaviorOf = { ctx, key, value -> - GlobalMeta.modifyMeta(key, value) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + GlobalMeta.modifyMeta(userId, key, value) ctx.status(200) }, withResults = { 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 883285047..2e626c02c 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 @@ -27,6 +30,7 @@ object SettingsController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(About.getAbout()) }, withResults = { @@ -43,6 +47,7 @@ object SettingsController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { AppUpdate.checkUpdate() } ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/GlobalMeta.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/GlobalMeta.kt index fe300f84c..330075e6f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/GlobalMeta.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/GlobalMeta.kt @@ -1,8 +1,8 @@ package suwayomi.tachidesk.global.impl +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.global.model.table.GlobalMetaTable @@ -15,28 +15,29 @@ import suwayomi.tachidesk.global.model.table.GlobalMetaTable * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ object GlobalMeta { - fun modifyMeta(key: String, value: String) { + fun modifyMeta(userId: Int, key: String, value: String) { transaction { val meta = transaction { - GlobalMetaTable.select { GlobalMetaTable.key eq key } + GlobalMetaTable.select { GlobalMetaTable.key eq key and (GlobalMetaTable.user eq userId) } }.firstOrNull() if (meta == null) { GlobalMetaTable.insert { it[GlobalMetaTable.key] = key it[GlobalMetaTable.value] = value + it[GlobalMetaTable.user] = userId } } else { - GlobalMetaTable.update({ GlobalMetaTable.key eq key }) { + GlobalMetaTable.update({ GlobalMetaTable.key eq key and (GlobalMetaTable.user eq userId) }) { it[GlobalMetaTable.value] = value } } } } - fun getMetaMap(): Map { + fun getMetaMap(userId: Int): Map { return transaction { - GlobalMetaTable.selectAll() + GlobalMetaTable.select { GlobalMetaTable.user eq userId } .associate { it[GlobalMetaTable.key] to it[GlobalMetaTable.value] } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/GlobalMetaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/GlobalMetaTable.kt index 4872f5c15..ea94e990c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/GlobalMetaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/GlobalMetaTable.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.global.model.table * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption /** * Metadata storage for clients, server/global level. @@ -15,4 +16,5 @@ import org.jetbrains.exposed.dao.id.IntIdTable object GlobalMetaTable : IntIdTable() { val key = varchar("key", 256) val value = varchar("value", 4096) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index e1d0da0b7..4aab7b132 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -8,26 +8,32 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.CategoryNodeList import suwayomi.tachidesk.graphql.types.CategoryNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable +import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser class CategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val categories = CategoryTable.select { CategoryTable.id inList ids } + val categories = CategoryTable.select { CategoryTable.id inList ids and (CategoryTable.user eq userId) } .map { CategoryType(it) } .associateBy { it.id } ids.map { categories[it] } @@ -38,12 +44,13 @@ class CategoryDataLoader : KotlinDataLoader { class CategoriesForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoriesForMangaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable) - .select { CategoryMangaTable.manga inList ids } + .select { CategoryMangaTable.manga inList ids and (CategoryMangaTable.user eq userId) and (CategoryTable.user eq userId) } .map { Pair(it[CategoryMangaTable.manga].value, CategoryType(it)) } .groupBy { it.first } .mapValues { it.value.map { pair -> pair.second } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 89a2af0a2..2c51773eb 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -8,25 +8,32 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ChapterNodeList import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser class ChapterDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val chapters = ChapterTable.select { ChapterTable.id inList ids } + val chapters = ChapterTable.getWithUserData(userId) + .select { ChapterTable.id inList ids } .map { ChapterType(it) } .associateBy { it.id } ids.map { chapters[it] } @@ -37,11 +44,13 @@ class ChapterDataLoader : KotlinDataLoader { class ChaptersForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChaptersForMangaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val chaptersByMangaId = ChapterTable.select { ChapterTable.manga inList ids } + val chaptersByMangaId = ChapterTable.getWithUserData(userId) + .select { ChapterTable.manga inList ids } .map { ChapterType(it) } .groupBy { it.mangaId } ids.map { (chaptersByMangaId[it] ?: emptyList()).toNodeList() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt index 7e8c0763c..619220184 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -21,7 +22,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future class ExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -36,7 +37,7 @@ class ExtensionDataLoader : KotlinDataLoader { class ExtensionForSourceDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionForSourceDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 491291806..dc549c6ff 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -8,27 +8,36 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser class MangaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val manga = MangaTable.select { MangaTable.id inList ids } + val manga = MangaTable.getWithUserData(userId) + .select { MangaTable.id inList ids } .map { MangaType(it) } .associateBy { it.id } ids.map { manga[it] } @@ -39,14 +48,15 @@ class MangaDataLoader : KotlinDataLoader { class MangaForCategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaForCategoryDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val itemsByRef = if (ids.contains(0)) { - MangaTable + MangaTable.getWithUserData(userId) .leftJoin(CategoryMangaTable) - .select { MangaTable.inLibrary eq true } + .select { MangaUserTable.inLibrary eq true and (CategoryMangaTable.user eq userId) } .andWhere { CategoryMangaTable.manga.isNull() } .map { MangaType(it) } .let { @@ -55,7 +65,7 @@ class MangaForCategoryDataLoader : KotlinDataLoader { } else { emptyMap() } + CategoryMangaTable.innerJoin(MangaTable) - .select { CategoryMangaTable.category inList ids } + .select { CategoryMangaTable.category inList ids and (CategoryMangaTable.user eq userId) } .map { Pair(it[CategoryMangaTable.category].value, MangaType(it)) } .groupBy { it.first } .mapValues { it.value.map { pair -> pair.second } } @@ -68,11 +78,13 @@ class MangaForCategoryDataLoader : KotlinDataLoader { class MangaForSourceDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaForSourceDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { + val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val mangaBySourceId = MangaTable.select { MangaTable.sourceReference inList ids } + val mangaBySourceId = MangaTable.getWithUserData(userId) + .select { MangaTable.sourceReference inList ids } .map { MangaType(it) } .groupBy { it.sourceId } ids.map { (mangaBySourceId[it] ?: emptyList()).toNodeList() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 662967dc9..d58e35a8a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -19,7 +20,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future class GlobalMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "GlobalMetaDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -34,7 +35,7 @@ class GlobalMetaDataLoader : KotlinDataLoader { class ChapterMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "ChapterMetaDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -49,7 +50,7 @@ class ChapterMetaDataLoader : KotlinDataLoader> { class MangaMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "MangaMetaDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -64,7 +65,7 @@ class MangaMetaDataLoader : KotlinDataLoader> { class CategoryMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "CategoryMetaDataLoader" - override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index bc58ac6f6..611ad1116 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader +import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -23,7 +24,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future class SourceDataLoader : KotlinDataLoader { override val dataLoaderName = "SourceDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -38,7 +39,7 @@ class SourceDataLoader : KotlinDataLoader { class SourcesForExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "SourcesForExtensionDataLoader" - override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) 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 156724ca5..593ea421d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/BackupMutation.kt @@ -1,5 +1,6 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment import io.javalin.http.UploadedFile import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope @@ -7,13 +8,16 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.server.TemporaryFileStorage +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.BackupRestoreState 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.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -29,13 +33,15 @@ class BackupMutation { @OptIn(DelicateCoroutinesApi::class) fun restoreBackup( + dataFetchingEnvironment: DataFetchingEnvironment, input: RestoreBackupInput ): CompletableFuture { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, backup) = input return future { GlobalScope.launch { - ProtoBackupImport.performRestore(backup.content) + ProtoBackupImport.performRestore(userId, backup.content) } val status = withTimeout(10.seconds) { @@ -58,11 +64,14 @@ class BackupMutation { val url: String ) fun createBackup( + dataFetchingEnvironment: DataFetchingEnvironment, input: CreateBackupInput? = null ): CreateBackupPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val filename = ProtoBackupExport.getBackupFilename() val backup = ProtoBackupExport.createBackup( + userId, BackupFlags( includeManga = true, includeCategories = input?.includeCategories ?: true, 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 161990cdf..38b9faf2b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/CategoryMutation.kt @@ -1,5 +1,6 @@ package suwayomi.tachidesk.graphql.mutations +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 @@ -9,9 +10,9 @@ import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +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 +24,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.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.requireUser class CategoryMutation { data class SetCategoryMetaInput( @@ -34,11 +38,13 @@ class CategoryMutation { val meta: CategoryMetaType ) fun setCategoryMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: SetCategoryMetaInput ): SetCategoryMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input - Category.modifyMeta(meta.categoryId, meta.key, meta.value) + Category.modifyMeta(userId, meta.categoryId, meta.key, meta.value) return SetCategoryMetaPayload(clientMutationId, meta) } @@ -54,15 +60,24 @@ class CategoryMutation { val category: CategoryType ) fun deleteCategoryMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: DeleteCategoryMetaInput ): DeleteCategoryMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId, key) = input val (meta, category) = transaction { - val meta = CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } - .firstOrNull() - - CategoryMetaTable.deleteWhere { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } + val meta = CategoryMetaTable.select { + CategoryMetaTable.user eq userId and + (CategoryMetaTable.ref eq categoryId) and + (CategoryMetaTable.key eq key) + }.firstOrNull() + + CategoryMetaTable.deleteWhere { + CategoryMetaTable.user eq userId and + (CategoryMetaTable.ref eq categoryId) and + (CategoryMetaTable.key eq key) + } val category = transaction { CategoryType(CategoryTable.select { CategoryTable.id eq categoryId }.first()) @@ -104,24 +119,24 @@ class CategoryMutation { val patch: UpdateCategoryPatch ) - private fun updateCategories(ids: List, patch: UpdateCategoryPatch) { + private fun updateCategories(userId: Int, ids: List, patch: UpdateCategoryPatch) { transaction { if (patch.name != null) { - CategoryTable.update({ CategoryTable.id inList ids }) { update -> + CategoryTable.update({ CategoryTable.id inList ids and (CategoryTable.user eq userId) }) { update -> patch.name.also { update[name] = it } } } if (patch.default != null) { - CategoryTable.update({ CategoryTable.id inList ids }) { update -> + CategoryTable.update({ CategoryTable.id inList ids and (CategoryTable.user eq userId) }) { update -> patch.default.also { update[isDefault] = it } } } if (patch.includeInUpdate != null) { - CategoryTable.update({ CategoryTable.id inList ids }) { update -> + CategoryTable.update({ CategoryTable.id inList ids and (CategoryTable.user eq userId) }) { update -> patch.includeInUpdate.also { update[includeInUpdate] = it.value } @@ -130,10 +145,14 @@ class CategoryMutation { } } - fun updateCategory(input: UpdateCategoryInput): UpdateCategoryPayload { + fun updateCategory( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryInput + ): UpdateCategoryPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input - updateCategories(listOf(id), patch) + updateCategories(userId, listOf(id), patch) val category = transaction { CategoryType(CategoryTable.select { CategoryTable.id eq id }.first()) @@ -145,10 +164,14 @@ class CategoryMutation { ) } - fun updateCategories(input: UpdateCategoriesInput): UpdateCategoriesPayload { + fun updateCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoriesInput + ): UpdateCategoriesPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input - updateCategories(ids, patch) + updateCategories(userId, ids, patch) val categories = transaction { CategoryTable.select { CategoryTable.id inList ids }.map { CategoryType(it) } @@ -170,7 +193,11 @@ class CategoryMutation { val position: Int ) - fun updateCategoryOrder(input: UpdateCategoryOrderInput): UpdateCategoryOrderPayload { + fun updateCategoryOrder( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryOrderInput + ): UpdateCategoryOrderPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId, position) = input require(position > 0) { "'order' must not be <= 0" @@ -178,30 +205,30 @@ class CategoryMutation { transaction { val currentOrder = CategoryTable - .select { CategoryTable.id eq categoryId } + .select { CategoryTable.id eq categoryId and (CategoryTable.user eq userId) } .first()[CategoryTable.order] if (currentOrder != position) { if (position < currentOrder) { - CategoryTable.update({ CategoryTable.order greaterEq position }) { + CategoryTable.update({ CategoryTable.order greaterEq position and (CategoryTable.user eq userId) }) { it[CategoryTable.order] = CategoryTable.order + 1 } } else { - CategoryTable.update({ CategoryTable.order lessEq position }) { + CategoryTable.update({ CategoryTable.order lessEq position and (CategoryTable.user eq userId) }) { it[CategoryTable.order] = CategoryTable.order - 1 } } - CategoryTable.update({ CategoryTable.id eq categoryId }) { + CategoryTable.update({ CategoryTable.id eq categoryId and (CategoryTable.user eq userId) }) { it[CategoryTable.order] = position } } } - Category.normalizeCategories() + Category.normalizeCategories(userId) val categories = transaction { - CategoryTable.selectAll().orderBy(CategoryTable.order).map { CategoryType(it) } + CategoryTable.select { CategoryTable.user eq userId }.orderBy(CategoryTable.order).map { CategoryType(it) } } return UpdateCategoryOrderPayload( @@ -222,11 +249,13 @@ class CategoryMutation { val category: CategoryType ) fun createCategory( + dataFetchingEnvironment: DataFetchingEnvironment, input: CreateCategoryInput ): CreateCategoryPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, name, order, default, includeInUpdate) = input transaction { - require(CategoryTable.select { CategoryTable.name eq input.name }.isEmpty()) { + require(CategoryTable.select { CategoryTable.name eq input.name and (CategoryTable.user eq userId) }.isEmpty()) { "'name' must be unique" } } @@ -241,12 +270,13 @@ class CategoryMutation { val category = transaction { if (order != null) { - CategoryTable.update({ CategoryTable.order greaterEq order }) { + CategoryTable.update({ CategoryTable.order greaterEq order and (CategoryTable.user eq userId) }) { it[CategoryTable.order] = CategoryTable.order + 1 } } val id = CategoryTable.insertAndGetId { + it[CategoryTable.user] = userId it[CategoryTable.name] = input.name it[CategoryTable.order] = order ?: Int.MAX_VALUE if (default != null) { @@ -257,9 +287,9 @@ class CategoryMutation { } } - Category.normalizeCategories() + Category.normalizeCategories(userId) - CategoryType(CategoryTable.select { CategoryTable.id eq id }.first()) + CategoryType(CategoryTable.select { CategoryTable.id eq id and (CategoryTable.user eq userId) }.first()) } return CreateCategoryPayload(clientMutationId, category) @@ -275,8 +305,10 @@ class CategoryMutation { val mangas: List ) fun deleteCategory( + dataFetchingEnvironment: DataFetchingEnvironment, input: DeleteCategoryInput ): DeleteCategoryPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, categoryId) = input if (categoryId == 0) { // Don't delete default category return DeleteCategoryPayload( @@ -287,18 +319,18 @@ class CategoryMutation { } val (category, mangas) = transaction { - val category = CategoryTable.select { CategoryTable.id eq categoryId } + val category = CategoryTable.select { CategoryTable.id eq categoryId and (CategoryTable.user eq userId) } .firstOrNull() val mangas = transaction { - MangaTable.innerJoin(CategoryMangaTable) - .select { CategoryMangaTable.category eq categoryId } + MangaTable.getWithUserData(userId).innerJoin(CategoryMangaTable) + .select { CategoryMangaTable.category eq categoryId and (CategoryMangaTable.user eq userId) } .map { MangaType(it) } } - CategoryTable.deleteWhere { CategoryTable.id eq categoryId } + CategoryTable.deleteWhere { CategoryTable.id eq categoryId and (CategoryTable.user eq userId) } - Category.normalizeCategories() + Category.normalizeCategories(userId) if (category != null) { CategoryType(category) @@ -336,13 +368,15 @@ class CategoryMutation { val patch: UpdateMangaCategoriesPatch ) - private fun updateMangas(ids: List, patch: UpdateMangaCategoriesPatch) { + private fun updateMangas(userId: Int, ids: List, patch: UpdateMangaCategoriesPatch) { transaction { if (patch.clearCategories == true) { - CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids } + CategoryMangaTable.deleteWhere { CategoryMangaTable.manga inList ids and (CategoryMangaTable.user eq userId) } } else if (!patch.removeFromCategories.isNullOrEmpty()) { CategoryMangaTable.deleteWhere { - (CategoryMangaTable.manga inList ids) and (CategoryMangaTable.category inList patch.removeFromCategories) + (CategoryMangaTable.manga inList ids) and + (CategoryMangaTable.category inList patch.removeFromCategories) and + (CategoryMangaTable.user eq userId) } } if (!patch.addToCategories.isNullOrEmpty()) { @@ -350,7 +384,9 @@ class CategoryMutation { ids.forEach { mangaId -> patch.addToCategories.forEach { categoryId -> val existingMapping = CategoryMangaTable.select { - (CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId) + (CategoryMangaTable.manga eq mangaId) and + (CategoryMangaTable.category eq categoryId) and + (CategoryMangaTable.user eq userId) }.isNotEmpty() if (!existingMapping) { @@ -363,18 +399,23 @@ class CategoryMutation { CategoryMangaTable.batchInsert(newCategories) { (manga, category) -> this[CategoryMangaTable.manga] = manga this[CategoryMangaTable.category] = category + this[CategoryMangaTable.user] = userId } } } } - fun updateMangaCategories(input: UpdateMangaCategoriesInput): UpdateMangaCategoriesPayload { + fun updateMangaCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangaCategoriesInput + ): UpdateMangaCategoriesPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input - updateMangas(listOf(id), patch) + updateMangas(userId, listOf(id), patch) val manga = transaction { - MangaType(MangaTable.select { MangaTable.id eq id }.first()) + MangaType(MangaTable.getWithUserData(userId).select { MangaTable.id eq id }.first()) } return UpdateMangaCategoriesPayload( @@ -383,13 +424,17 @@ class CategoryMutation { ) } - fun updateMangasCategories(input: UpdateMangasCategoriesInput): UpdateMangasCategoriesPayload { + fun updateMangasCategories( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangasCategoriesInput + ): UpdateMangasCategoriesPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input - updateMangas(ids, patch) + updateMangas(userId, ids, patch) val mangas = transaction { - MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) } + MangaTable.getWithUserData(userId).select { MangaTable.id inList ids }.map { MangaType(it) } } return UpdateMangasCategoriesPayload( 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 5361830ce..275d37111 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 eu.kanade.tachiyomi.source.model.SChapter +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.batchInsert @@ -8,6 +9,7 @@ import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.impl.Chapter @@ -15,9 +17,13 @@ import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.PageTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser import java.time.Instant import java.util.concurrent.CompletableFuture @@ -53,11 +59,19 @@ class ChapterMutation { val patch: UpdateChapterPatch ) - private fun updateChapters(ids: List, patch: UpdateChapterPatch) { + private fun updateChapters(userId: Int, ids: List, patch: UpdateChapterPatch) { transaction { + val currentChapterUserItems = ChapterUserTable.select { ChapterUserTable.chapter inList ids } + .map { it[ChapterUserTable.chapter].value } + if (currentChapterUserItems.size < ids.size) { + ChapterUserTable.batchInsert(ids - currentChapterUserItems.toSet()) { + this[ChapterUserTable.user] = userId + this[ChapterUserTable.chapter] = it + } + } if (patch.isRead != null || patch.isBookmarked != null || patch.lastPageRead != null) { val now = Instant.now().epochSecond - ChapterTable.update({ ChapterTable.id inList ids }) { update -> + ChapterUserTable.update({ ChapterUserTable.chapter inList ids }) { update -> patch.isRead?.also { update[isRead] = it } @@ -74,14 +88,16 @@ class ChapterMutation { } fun updateChapter( + dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChapterInput ): UpdateChapterPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input - updateChapters(listOf(id), patch) + updateChapters(userId, listOf(id), patch) val chapter = transaction { - ChapterType(ChapterTable.select { ChapterTable.id eq id }.first()) + ChapterType(ChapterTable.getWithUserData(userId).select { ChapterTable.id eq id }.first()) } return UpdateChapterPayload( @@ -91,14 +107,16 @@ class ChapterMutation { } fun updateChapters( + dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateChaptersInput ): UpdateChaptersPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input - updateChapters(ids, patch) + updateChapters(userId, ids, patch) val chapters = transaction { - ChapterTable.select { ChapterTable.id inList ids }.map { ChapterType(it) } + ChapterTable.getWithUserData(userId).select { ChapterTable.id inList ids }.map { ChapterType(it) } } return UpdateChaptersPayload( @@ -117,17 +135,19 @@ class ChapterMutation { ) fun fetchChapters( + dataFetchingEnvironment: DataFetchingEnvironment, input: FetchChaptersInput ): CompletableFuture { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId) = input return future { val numberOfCurrentChapters = Chapter.getCountOfMangaChapters(mangaId) - Chapter.fetchChapterList(mangaId) + Chapter.fetchChapterList(userId, mangaId) numberOfCurrentChapters }.thenApply { numberOfCurrentChapters -> val chapters = transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } + ChapterTable.getWithUserData(userId).select { ChapterTable.manga eq mangaId } .orderBy(ChapterTable.sourceOrder) } @@ -150,11 +170,13 @@ class ChapterMutation { val meta: ChapterMetaType ) fun setChapterMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: SetChapterMetaInput ): SetChapterMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input - Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value) + Chapter.modifyChapterMeta(userId, meta.chapterId, meta.key, meta.value) return SetChapterMetaPayload(clientMutationId, meta) } @@ -170,18 +192,27 @@ class ChapterMutation { val chapter: ChapterType ) fun deleteChapterMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: DeleteChapterMetaInput ): DeleteChapterMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapterId, key) = input val (meta, chapter) = transaction { - val meta = ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } - .firstOrNull() + val meta = ChapterMetaTable.select { + ChapterMetaTable.user eq userId and + (ChapterMetaTable.ref eq chapterId) and + (ChapterMetaTable.key eq key) + }.firstOrNull() - ChapterMetaTable.deleteWhere { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } + ChapterMetaTable.deleteWhere { + ChapterMetaTable.user eq userId and + (ChapterMetaTable.ref eq chapterId) and + (ChapterMetaTable.key eq key) + } val chapter = transaction { - ChapterType(ChapterTable.select { ChapterTable.id eq chapterId }.first()) + ChapterType(ChapterTable.getWithUserData(userId).select { ChapterTable.id eq chapterId }.first()) } if (meta != null) { @@ -204,8 +235,10 @@ class ChapterMutation { val chapter: ChapterType ) fun fetchChapterPages( + dataFetchingEnvironment: DataFetchingEnvironment, input: FetchChapterPagesInput ): CompletableFuture { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapterId) = input val chapter = transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() } @@ -241,7 +274,7 @@ class ChapterMutation { "/api/v1/manga/$mangaId/chapter/$chapterIndex/page/$index" }, chapter = ChapterType( - transaction { ChapterTable.select { ChapterTable.id eq chapterId }.first() } + transaction { ChapterTable.getWithUserData(userId).select { ChapterTable.id eq chapterId }.first() } ) ) } 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 6211fae71..11d46e028 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/ExtensionMutation.kt @@ -1,13 +1,17 @@ package suwayomi.tachidesk.graphql.mutations import eu.kanade.tachiyomi.source.local.LocalSource +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +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 import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class ExtensionMutation { @@ -62,7 +66,11 @@ class ExtensionMutation { } } - fun updateExtension(input: UpdateExtensionInput): CompletableFuture { + fun updateExtension( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateExtensionInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input return future { @@ -79,7 +87,11 @@ class ExtensionMutation { } } - fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture { + fun updateExtensions( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateExtensionsInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input return future { @@ -106,8 +118,10 @@ class ExtensionMutation { ) fun fetchExtensions( + dataFetchingEnvironment: DataFetchingEnvironment, input: FetchExtensionsInput ): CompletableFuture { + dataFetchingEnvironment.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() val (clientMutationId) = input return future { 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 569180cb4..334adc0d8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MangaMutation.kt @@ -1,17 +1,25 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.impl.Manga import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser +import java.time.Instant import java.util.concurrent.CompletableFuture /** @@ -44,25 +52,42 @@ class MangaMutation { val patch: UpdateMangaPatch ) - private fun updateMangas(ids: List, patch: UpdateMangaPatch) { + private fun updateMangas(userId: Int, ids: List, patch: UpdateMangaPatch) { transaction { + val currentMangaUserItems = MangaUserTable.select { MangaUserTable.manga inList ids } + .map { it[MangaUserTable.manga].value } + if (currentMangaUserItems.size < ids.size) { + MangaUserTable.batchInsert(ids - currentMangaUserItems.toSet()) { + this[MangaUserTable.user] = userId + this[MangaUserTable.manga] = it + } + } + if (patch.inLibrary != null) { - MangaTable.update({ MangaTable.id inList ids }) { update -> + val now = Instant.now().epochSecond + MangaUserTable.update({ MangaUserTable.manga inList ids }) { update -> patch.inLibrary.also { update[inLibrary] = it + if (patch.inLibrary) { + update[inLibraryAt] = now + } } } } } } - fun updateManga(input: UpdateMangaInput): UpdateMangaPayload { + fun updateManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangaInput + ): UpdateMangaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id, patch) = input - updateMangas(listOf(id), patch) + updateMangas(userId, listOf(id), patch) val manga = transaction { - MangaType(MangaTable.select { MangaTable.id eq id }.first()) + MangaType(MangaTable.getWithUserData(userId).select { MangaTable.id eq id }.first()) } return UpdateMangaPayload( @@ -71,13 +96,17 @@ class MangaMutation { ) } - fun updateMangas(input: UpdateMangasInput): UpdateMangasPayload { + fun updateMangas( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateMangasInput + ): UpdateMangasPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, ids, patch) = input - updateMangas(ids, patch) + updateMangas(userId, ids, patch) val mangas = transaction { - MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) } + MangaTable.getWithUserData(userId).select { MangaTable.id inList ids }.map { MangaType(it) } } return UpdateMangasPayload( @@ -96,15 +125,17 @@ class MangaMutation { ) fun fetchManga( + dataFetchingEnvironment: DataFetchingEnvironment, input: FetchMangaInput ): CompletableFuture { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, id) = input return future { Manga.fetchManga(id) }.thenApply { val manga = transaction { - MangaTable.select { MangaTable.id eq id }.first() + MangaTable.getWithUserData(userId).select { MangaTable.id eq id }.first() } FetchMangaPayload( clientMutationId = clientMutationId, @@ -122,11 +153,13 @@ class MangaMutation { val meta: MangaMetaType ) fun setMangaMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: SetMangaMetaInput ): SetMangaMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input - Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value) + Manga.modifyMangaMeta(userId, meta.mangaId, meta.key, meta.value) return SetMangaMetaPayload(clientMutationId, meta) } @@ -142,18 +175,27 @@ class MangaMutation { val manga: MangaType ) fun deleteMangaMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: DeleteMangaMetaInput ): DeleteMangaMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaId, key) = input val (meta, manga) = transaction { - val meta = MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } - .firstOrNull() - - MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } + val meta = MangaMetaTable.select { + MangaMetaTable.user eq userId and + (MangaMetaTable.ref eq mangaId) and + (MangaMetaTable.key eq key) + }.firstOrNull() + + MangaMetaTable.deleteWhere { + MangaMetaTable.user eq userId and + (MangaMetaTable.ref eq mangaId) and + (MangaMetaTable.key eq key) + } val manga = transaction { - MangaType(MangaTable.select { MangaTable.id eq mangaId }.first()) + MangaType(MangaTable.getWithUserData(userId).select { MangaTable.id eq mangaId }.first()) } if (meta != null) { 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 57c4238bf..34712c0f2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/MetaMutation.kt @@ -1,12 +1,17 @@ package suwayomi.tachidesk.graphql.mutations +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.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.model.table.GlobalMetaTable +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.GlobalMetaType +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.requireUser class MetaMutation { @@ -19,11 +24,13 @@ class MetaMutation { val meta: GlobalMetaType ) fun setGlobalMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: SetGlobalMetaInput ): SetGlobalMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, meta) = input - GlobalMeta.modifyMeta(meta.key, meta.value) + GlobalMeta.modifyMeta(userId, meta.key, meta.value) return SetGlobalMetaPayload(clientMutationId, meta) } @@ -37,15 +44,17 @@ class MetaMutation { val meta: GlobalMetaType? ) fun deleteGlobalMeta( + dataFetchingEnvironment: DataFetchingEnvironment, input: DeleteGlobalMetaInput ): DeleteGlobalMetaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, key) = input val meta = transaction { - val meta = GlobalMetaTable.select { GlobalMetaTable.key eq key } + val meta = GlobalMetaTable.select { GlobalMetaTable.key eq key and (GlobalMetaTable.user eq userId) } .firstOrNull() - GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key } + GlobalMetaTable.deleteWhere { GlobalMetaTable.key eq key and (GlobalMetaTable.user eq userId) } if (meta != null) { GlobalMetaType(meta) 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 6c378eeaf..b972ede15 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -5,8 +5,10 @@ import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.SwitchPreferenceCompat +import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.Preference @@ -17,7 +19,10 @@ import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class SourceMutation { @@ -42,8 +47,10 @@ class SourceMutation { ) fun fetchSourceManga( + dataFetchingEnvironment: DataFetchingEnvironment, input: FetchSourceMangaInput ): CompletableFuture { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, sourceId, type, page, query, filters) = input return future { @@ -68,7 +75,7 @@ class SourceMutation { val mangaIds = mangasPage.insertOrGet(sourceId) val mangas = transaction { - MangaTable.select { MangaTable.id inList mangaIds } + MangaTable.getWithUserData(userId).select { MangaTable.id inList mangaIds } .map { MangaType(it) } }.sortedBy { mangaIds.indexOf(it.id) @@ -101,8 +108,10 @@ class SourceMutation { ) fun updateSourcePreference( + dataFetchingEnvironment: DataFetchingEnvironment, input: UpdateSourcePreferenceInput ): UpdateSourcePreferencePayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, sourceId, change) = input Source.setSourcePreference(sourceId, change.position, "") { preference -> 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 f5fa35078..ce1cf7054 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,14 @@ 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.user.requireUser class BackupQuery { data class ValidateBackupInput( @@ -18,15 +22,18 @@ class BackupQuery { val missingSources: List ) 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) } ) } - fun restoreStatus(): BackupRestoreStatus { + fun restoreStatus(dataFetchingEnvironment: DataFetchingEnvironment): BackupRestoreStatus { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return ProtoBackupImport.backupRestoreState.value.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 d1c64d203..f9a06c0c4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/CategoryQuery.kt @@ -15,7 +15,7 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter import suwayomi.tachidesk.graphql.queries.filter.Filter @@ -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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -37,10 +38,13 @@ 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.user.requireUser import java.util.concurrent.CompletableFuture class CategoryQuery { fun category(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) } @@ -112,6 +116,7 @@ class CategoryQuery { } fun categories( + dataFetchingEnvironment: DataFetchingEnvironment, condition: CategoryCondition? = null, filter: CategoryFilter? = null, orderBy: CategoryOrderBy? = null, @@ -122,8 +127,9 @@ class CategoryQuery { last: Int? = null, offset: Int? = null ): CategoryNodeList { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { - val res = CategoryTable.selectAll() + val res = CategoryTable.select { CategoryTable.user eq userId } res.applyOps(condition, filter) 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 f3aedcfd5..6e1ecf74e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/ChapterQuery.kt @@ -12,9 +12,11 @@ import graphql.schema.DataFetchingEnvironment import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Op import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.queries.filter.BooleanFilter @@ -29,6 +31,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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -39,7 +42,12 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap 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.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture /** @@ -49,6 +57,7 @@ import java.util.concurrent.CompletableFuture */ class ChapterQuery { fun chapter(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("ChapterDataLoader", id) } @@ -58,7 +67,7 @@ class ChapterQuery { NAME(ChapterTable.name), UPLOAD_DATE(ChapterTable.date_upload), CHAPTER_NUMBER(ChapterTable.chapter_number), - LAST_READ_AT(ChapterTable.lastReadAt), + LAST_READ_AT(ChapterUserTable.lastReadAt), FETCHED_AT(ChapterTable.fetchedAt); override fun greater(cursor: Cursor): Op { @@ -68,7 +77,7 @@ class ChapterQuery { NAME -> greaterNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString) UPLOAD_DATE -> greaterNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong) CHAPTER_NUMBER -> greaterNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat) - LAST_READ_AT -> greaterNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong) + LAST_READ_AT -> greaterNotUnique(ChapterUserTable.lastReadAt, ChapterTable.id, cursor, String::toLong) FETCHED_AT -> greaterNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong) } } @@ -80,7 +89,7 @@ class ChapterQuery { NAME -> lessNotUnique(ChapterTable.name, ChapterTable.id, cursor, String::toString) UPLOAD_DATE -> lessNotUnique(ChapterTable.date_upload, ChapterTable.id, cursor, String::toLong) CHAPTER_NUMBER -> lessNotUnique(ChapterTable.chapter_number, ChapterTable.id, cursor, String::toFloat) - LAST_READ_AT -> lessNotUnique(ChapterTable.lastReadAt, ChapterTable.id, cursor, String::toLong) + LAST_READ_AT -> lessNotUnique(ChapterUserTable.lastReadAt, ChapterTable.id, cursor, String::toLong) FETCHED_AT -> lessNotUnique(ChapterTable.fetchedAt, ChapterTable.id, cursor, String::toLong) } } @@ -126,10 +135,10 @@ class ChapterQuery { opAnd.eq(chapterNumber, ChapterTable.chapter_number) opAnd.eq(scanlator, ChapterTable.scanlator) opAnd.eq(mangaId, ChapterTable.manga) - opAnd.eq(isRead, ChapterTable.isRead) - opAnd.eq(isBookmarked, ChapterTable.isBookmarked) - opAnd.eq(lastPageRead, ChapterTable.lastPageRead) - opAnd.eq(lastReadAt, ChapterTable.lastReadAt) + opAnd.eq(isRead, ChapterUserTable.isRead) + opAnd.eq(isBookmarked, ChapterUserTable.isBookmarked) + opAnd.eq(lastPageRead, ChapterUserTable.lastPageRead) + opAnd.eq(lastReadAt, ChapterUserTable.lastReadAt) opAnd.eq(sourceOrder, ChapterTable.sourceOrder) opAnd.eq(realUrl, ChapterTable.realUrl) opAnd.eq(fetchedAt, ChapterTable.fetchedAt) @@ -171,10 +180,10 @@ class ChapterQuery { andFilterWithCompare(ChapterTable.chapter_number, chapterNumber), andFilterWithCompareString(ChapterTable.scanlator, scanlator), andFilterWithCompareEntity(ChapterTable.manga, mangaId), - andFilterWithCompare(ChapterTable.isRead, isRead), - andFilterWithCompare(ChapterTable.isBookmarked, isBookmarked), - andFilterWithCompare(ChapterTable.lastPageRead, lastPageRead), - andFilterWithCompare(ChapterTable.lastReadAt, lastReadAt), + andFilterWithCompare(ChapterUserTable.isRead, isRead), + andFilterWithCompare(ChapterUserTable.isBookmarked, isBookmarked), + andFilterWithCompare(ChapterUserTable.lastPageRead, lastPageRead), + andFilterWithCompare(ChapterUserTable.lastReadAt, lastReadAt), andFilterWithCompare(ChapterTable.sourceOrder, sourceOrder), andFilterWithCompareString(ChapterTable.realUrl, realUrl), andFilterWithCompare(ChapterTable.fetchedAt, fetchedAt), @@ -183,10 +192,11 @@ class ChapterQuery { ) } - fun getLibraryOp() = andFilterWithCompare(MangaTable.inLibrary, inLibrary) + fun getLibraryOp() = andFilterWithCompare(MangaUserTable.inLibrary, inLibrary) } fun chapters( + dataFetchingEnvironment: DataFetchingEnvironment, condition: ChapterCondition? = null, filter: ChapterFilter? = null, orderBy: ChapterOrderBy? = null, @@ -197,13 +207,14 @@ class ChapterQuery { last: Int? = null, offset: Int? = null ): ChapterNodeList { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { - val res = ChapterTable.selectAll() + val res = ChapterTable.getWithUserData(userId).selectAll() val libraryOp = filter?.getLibraryOp() if (libraryOp != null) { res.adjustColumnSet { - innerJoin(MangaTable) + innerJoin(MangaTable.getWithUserData(userId)) } res.andWhere { libraryOp } } 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 bb49f6b77..c479b84de 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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -38,10 +39,13 @@ 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.user.requireUser import java.util.concurrent.CompletableFuture class ExtensionQuery { fun extension(dataFetchingEnvironment: DataFetchingEnvironment, pkgName: String): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) } @@ -140,6 +144,7 @@ class ExtensionQuery { } fun extensions( + dataFetchingEnvironment: DataFetchingEnvironment, condition: ExtensionCondition? = null, filter: ExtensionFilter? = null, orderBy: ExtensionOrderBy? = null, @@ -150,6 +155,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/MangaQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt index 21d9eb1dc..7d39ef777 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MangaQuery.kt @@ -29,6 +29,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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -40,24 +41,29 @@ import suwayomi.tachidesk.graphql.types.MangaNodeList import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture class MangaQuery { fun manga(dataFetchingEnvironment: DataFetchingEnvironment, id: Int): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) } enum class MangaOrderBy(override val column: Column>) : OrderBy { ID(MangaTable.id), TITLE(MangaTable.title), - IN_LIBRARY_AT(MangaTable.inLibraryAt), + IN_LIBRARY_AT(MangaUserTable.inLibraryAt), LAST_FETCHED_AT(MangaTable.lastFetchedAt); override fun greater(cursor: Cursor): Op { return when (this) { ID -> MangaTable.id greater cursor.value.toInt() TITLE -> greaterNotUnique(MangaTable.title, MangaTable.id, cursor, String::toString) - IN_LIBRARY_AT -> greaterNotUnique(MangaTable.inLibraryAt, MangaTable.id, cursor, String::toLong) + IN_LIBRARY_AT -> greaterNotUnique(MangaUserTable.inLibraryAt, MangaTable.id, cursor, String::toLong) LAST_FETCHED_AT -> greaterNotUnique(MangaTable.lastFetchedAt, MangaTable.id, cursor, String::toLong) } } @@ -66,7 +72,7 @@ class MangaQuery { return when (this) { ID -> MangaTable.id less cursor.value.toInt() TITLE -> lessNotUnique(MangaTable.title, MangaTable.id, cursor, String::toString) - IN_LIBRARY_AT -> lessNotUnique(MangaTable.inLibraryAt, MangaTable.id, cursor, String::toLong) + IN_LIBRARY_AT -> lessNotUnique(MangaUserTable.inLibraryAt, MangaTable.id, cursor, String::toLong) LAST_FETCHED_AT -> lessNotUnique(MangaTable.lastFetchedAt, MangaTable.id, cursor, String::toLong) } } @@ -113,8 +119,8 @@ class MangaQuery { opAnd.eq(description, MangaTable.description) opAnd.eq(genre?.joinToString(), MangaTable.genre) opAnd.eq(status?.value, MangaTable.status) - opAnd.eq(inLibrary, MangaTable.inLibrary) - opAnd.eq(inLibraryAt, MangaTable.inLibraryAt) + opAnd.eq(inLibrary, MangaUserTable.inLibrary) + opAnd.eq(inLibraryAt, MangaUserTable.inLibraryAt) opAnd.eq(realUrl, MangaTable.realUrl) opAnd.eq(lastFetchedAt, MangaTable.lastFetchedAt) opAnd.eq(chaptersLastFetchedAt, MangaTable.chaptersLastFetchedAt) @@ -184,16 +190,17 @@ class MangaQuery { andFilterWithCompareString(MangaTable.author, author), andFilterWithCompareString(MangaTable.description, description), andFilterWithCompare(MangaTable.status, status?.asIntFilter()), - andFilterWithCompare(MangaTable.inLibrary, inLibrary), - andFilterWithCompare(MangaTable.inLibraryAt, inLibraryAt), + andFilterWithCompare(MangaUserTable.inLibrary, inLibrary), + andFilterWithCompare(MangaUserTable.inLibraryAt, inLibraryAt), andFilterWithCompareString(MangaTable.realUrl, realUrl), - andFilterWithCompare(MangaTable.inLibraryAt, lastFetchedAt), - andFilterWithCompare(MangaTable.inLibraryAt, chaptersLastFetchedAt) + andFilterWithCompare(MangaTable.lastFetchedAt, lastFetchedAt), + andFilterWithCompare(MangaTable.chaptersLastFetchedAt, chaptersLastFetchedAt) ) } } fun mangas( + dataFetchingEnvironment: DataFetchingEnvironment, condition: MangaCondition? = null, filter: MangaFilter? = null, orderBy: MangaOrderBy? = null, @@ -204,8 +211,9 @@ class MangaQuery { last: Int? = null, offset: Int? = null ): MangaNodeList { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { - val res = MangaTable.selectAll() + val res = MangaTable.getWithUserData(userId).selectAll() res.applyOps(condition, filter) 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 8c54d58ba..7f385df36 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/MetaQuery.kt @@ -15,7 +15,7 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater import org.jetbrains.exposed.sql.SqlExpressionBuilder.less import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.graphql.queries.filter.Filter @@ -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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -33,10 +34,13 @@ 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.user.requireUser import java.util.concurrent.CompletableFuture class MetaQuery { fun meta(dataFetchingEnvironment: DataFetchingEnvironment, key: String): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) } @@ -96,6 +100,7 @@ class MetaQuery { } fun metas( + dataFetchingEnvironment: DataFetchingEnvironment, condition: MetaCondition? = null, filter: MetaFilter? = null, orderBy: MetaOrderBy? = null, @@ -106,8 +111,9 @@ class MetaQuery { last: Int? = null, offset: Int? = null ): GlobalMetaNodeList { + val userId = dataFetchingEnvironment.graphQlContext.getAttribute(Attribute.TachideskUser).requireUser() val queryResults = transaction { - val res = GlobalMetaTable.selectAll() + val res = GlobalMetaTable.select { GlobalMetaTable.user eq userId } res.applyOps(condition, filter) 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 dfd9e8520..7bebe2f3f 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.OrderBy import suwayomi.tachidesk.graphql.server.primitives.PageInfo @@ -37,10 +38,14 @@ 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.getAttribute(Attribute.TachideskUser).requireUser() return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) } @@ -112,6 +117,7 @@ class SourceQuery { } fun sources( + dataFetchingEnvironment: DataFetchingEnvironment, condition: SourceCondition? = null, filter: SourceFilter? = null, orderBy: SourceOrderBy? = null, @@ -122,6 +128,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/server/TachideskDataFetcherExceptionHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt new file mode 100644 index 000000000..bc4a7af57 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt @@ -0,0 +1,26 @@ +package suwayomi.tachidesk.graphql.server + +import com.expediagroup.graphql.server.extensions.getFromContext +import graphql.ExceptionWhileDataFetching +import graphql.execution.DataFetcherExceptionHandlerParameters +import graphql.execution.DataFetcherExceptionHandlerResult +import graphql.execution.SimpleDataFetcherExceptionHandler +import io.javalin.http.Context +import io.javalin.http.HttpCode +import suwayomi.tachidesk.server.user.UnauthorizedException + +class TachideskDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() { + + @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult { + val exception = handlerParameters.exception + if (exception is UnauthorizedException) { + val error = ExceptionWhileDataFetching(handlerParameters.path, exception, handlerParameters.sourceLocation) + logException(error, exception) + // Set the HTTP status code to 401 + handlerParameters.dataFetchingEnvironment.getFromContext()?.status(HttpCode.UNAUTHORIZED) + return DataFetcherExceptionHandlerResult.newResult().error(error).build() + } + return super.onException(handlerParameters) + } +} 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 34147ae8d..12b0895f3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt @@ -7,35 +7,36 @@ package suwayomi.tachidesk.graphql.server -import com.expediagroup.graphql.generator.execution.GraphQLContext +import com.expediagroup.graphql.generator.extensions.toGraphQLContext 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 suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.JavalinSetup.getAttribute /** * Custom logic for how Tachidesk should create its context given the [Context] */ -class TachideskGraphQLContextFactory : GraphQLContextFactory { - override suspend fun generateContextMap(request: Context): Map<*, Any> = emptyMap() -// 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 -// } -// } +class TachideskGraphQLContextFactory : GraphQLContextFactory { + override suspend fun generateContext(request: Context): GraphQLContext { + return mapOf( + Context::class to request, + request.getPair(Attribute.TachideskUser) + ).toGraphQLContext() + } + + private fun Context.getPair(attribute: Attribute) = + attribute to getAttribute(attribute) fun generateContextMap(request: WsContext): Map<*, Any> = emptyMap() } -/** - * Create a [GraphQLContext] from [this] map - * @return a new [GraphQLContext] - */ -fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = - graphql.GraphQLContext.of(this) +fun GraphQLContext.getAttribute(attribute: Attribute): T { + return get(attribute) +} + +fun DataFetchingEnvironment.getAttribute(attribute: Attribute): T { + return graphQlContext.get(attribute) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt index 47d3e29eb..602a2cee9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLServer.kt @@ -44,6 +44,7 @@ class TachideskGraphQLServer( companion object { private fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(schema) .subscriptionExecutionStrategy(FlowSubscriptionExecutionStrategy()) + .defaultDataFetcherExceptionHandler(TachideskDataFetcherExceptionHandler()) .build() fun create(): TachideskGraphQLServer { 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 e8da2c603..1a22324f7 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 @@ -7,6 +7,7 @@ package suwayomi.tachidesk.graphql.server.subscriptions +import com.expediagroup.graphql.generator.extensions.toGraphQLContext import com.expediagroup.graphql.server.types.GraphQLRequest import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.convertValue @@ -37,7 +38,6 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_KEEP_ALIVE import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_DATA import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR -import suwayomi.tachidesk.graphql.server.toGraphQLContext /** * Implementation of the `graphql-ws` protocol defined by Apollo diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt index f9ec6b0f2..7a96a7ac4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt @@ -7,6 +7,7 @@ package suwayomi.tachidesk.graphql.server.subscriptions +import com.expediagroup.graphql.generator.extensions.toGraphQLContext import graphql.GraphQLContext import io.javalin.websocket.WsContext import kotlinx.coroutines.Job @@ -15,7 +16,6 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onCompletion import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_COMPLETE -import suwayomi.tachidesk.graphql.server.toGraphQLContext import java.util.concurrent.ConcurrentHashMap internal class ApolloSubscriptionSessionState { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt index a402e6421..5e92768c8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt @@ -29,8 +29,8 @@ open class GraphQLSubscriptionHandler( graphQLRequest: GraphQLRequest, graphQLContext: GraphQLContext = GraphQLContext.of(emptyMap()) ): Flow> { - val dataLoaderRegistry = dataLoaderRegistryFactory?.generate() - val input = graphQLRequest.toExecutionInput(dataLoaderRegistry, graphQLContext) + val dataLoaderRegistry = dataLoaderRegistryFactory?.generate(graphQLContext) + val input = graphQLRequest.toExecutionInput(graphQLContext, dataLoaderRegistry) val res = graphQL.execute(input) val data = res.getData>() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt index 9010bcaad..924a93f6b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/ChapterType.kt @@ -17,6 +17,7 @@ import suwayomi.tachidesk.graphql.server.primitives.NodeList import suwayomi.tachidesk.graphql.server.primitives.PageInfo import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import java.util.concurrent.CompletableFuture class ChapterType( @@ -46,10 +47,10 @@ class ChapterType( row[ChapterTable.chapter_number], row[ChapterTable.scanlator], row[ChapterTable.manga].value, - row[ChapterTable.isRead], - row[ChapterTable.isBookmarked], - row[ChapterTable.lastPageRead], - row[ChapterTable.lastReadAt], + row.getOrNull(ChapterUserTable.isRead) ?: false, + row.getOrNull(ChapterUserTable.isBookmarked) ?: false, + row.getOrNull(ChapterUserTable.lastPageRead) ?: 0, + row.getOrNull(ChapterUserTable.lastReadAt) ?: 0, row[ChapterTable.sourceOrder], row[ChapterTable.realUrl], row[ChapterTable.fetchedAt], diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt index c20f23243..9a4663ea5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MangaType.kt @@ -20,6 +20,7 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable import java.time.Instant import java.util.concurrent.CompletableFuture @@ -53,8 +54,8 @@ class MangaType( row[MangaTable.description], row[MangaTable.genre].toGenreList(), MangaStatus.valueOf(row[MangaTable.status]), - row[MangaTable.inLibrary], - row[MangaTable.inLibraryAt], + row.getOrNull(MangaUserTable.inLibrary) ?: false, + row.getOrNull(MangaUserTable.inLibraryAt) ?: 0, row[MangaTable.realUrl], row[MangaTable.lastFetchedAt], row[MangaTable.chaptersLastFetchedAt] 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 471575695..d2a8b28ca 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt @@ -5,7 +5,10 @@ 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.ProtoBackupValidator +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 @@ -27,9 +30,10 @@ object BackupController { } }, behaviorOf = { ctx -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) + ProtoBackupImport.performRestore(userId, ctx.bodyAsInputStream()) } ) }, @@ -52,9 +56,10 @@ object BackupController { }, behaviorOf = { ctx -> // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) + ProtoBackupImport.performRestore(userId, ctx.uploadedFile("backup.proto.gz")!!.content) } ) }, @@ -73,10 +78,12 @@ object BackupController { } }, behaviorOf = { ctx -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.contentType("application/octet-stream") ctx.future( future { ProtoBackupExport.createBackup( + userId, BackupFlags( includeManga = true, includeCategories = true, @@ -102,12 +109,14 @@ object BackupController { } }, behaviorOf = { ctx -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.contentType("application/octet-stream") ctx.header("Content-Disposition", """attachment; filename="${ProtoBackupExport.getBackupFilename()}"""") ctx.future( future { ProtoBackupExport.createBackup( + userId, BackupFlags( includeManga = true, includeCategories = true, @@ -133,6 +142,7 @@ object BackupController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { ProtoBackupValidator.validate(ctx.bodyAsInputStream()) @@ -157,6 +167,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 c4ec3ef3f..0f70fad83 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 @@ -27,7 +30,8 @@ object CategoryController { } }, behaviorOf = { ctx -> - ctx.json(Category.getCategoryList()) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.json(Category.getCategoryList(userId)) }, withResults = { json>(HttpCode.OK) @@ -44,7 +48,8 @@ object CategoryController { } }, behaviorOf = { ctx, name -> - if (Category.createCategory(name) != -1) { + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + if (Category.createCategory(userId, name) != -1) { ctx.status(200) } else { ctx.status(HttpCode.BAD_REQUEST) @@ -69,7 +74,8 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate -> - Category.updateCategory(categoryId, name, isDefault, includeInUpdate) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Category.updateCategory(userId, categoryId, name, isDefault, includeInUpdate) ctx.status(200) }, withResults = { @@ -87,7 +93,8 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId -> - Category.removeCategory(categoryId) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Category.removeCategory(userId, categoryId) ctx.status(200) }, withResults = { @@ -105,7 +112,8 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId -> - ctx.json(CategoryManga.getCategoryMangaList(categoryId)) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.json(CategoryManga.getCategoryMangaList(userId, categoryId)) }, withResults = { json>(HttpCode.OK) @@ -123,7 +131,8 @@ object CategoryController { } }, behaviorOf = { ctx, from, to -> - Category.reorderCategory(from, to) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Category.reorderCategory(userId, from, to) ctx.status(200) }, withResults = { @@ -143,7 +152,8 @@ object CategoryController { } }, behaviorOf = { ctx, categoryId, key, value -> - Category.modifyMeta(categoryId, key, value) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Category.modifyMeta(userId, categoryId, key, value) ctx.status(200) }, withResults = { 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 e501a720d..7c74ad4e4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt @@ -16,7 +16,10 @@ import org.kodein.di.conf.global import org.kodein.di.instance 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 @@ -46,7 +49,8 @@ object DownloadController { description("Start the downloader") } }, - behaviorOf = { + behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.start() }, withResults = { @@ -63,6 +67,7 @@ object DownloadController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { DownloadManager.stop() } ) @@ -81,6 +86,7 @@ object DownloadController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { DownloadManager.clear() } ) @@ -101,6 +107,7 @@ object DownloadController { } }, behaviorOf = { ctx, chapterIndex, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex) @@ -122,6 +129,7 @@ object DownloadController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val inputs = json.decodeFromString(ctx.body()) ctx.future( future { @@ -144,6 +152,7 @@ object DownloadController { body() }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) ctx.future( future { @@ -167,6 +176,7 @@ object DownloadController { } }, behaviorOf = { ctx, chapterIndex, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.unqueue(chapterIndex, mangaId) ctx.status(200) @@ -187,7 +197,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 4d736f166..d50217e50 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 mu.KotlinLogging 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 @@ -29,6 +32,7 @@ object ExtensionController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { ExtensionsList.getExtensionList() @@ -50,6 +54,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { Extension.installExtension(pkgName) @@ -76,6 +81,7 @@ object ExtensionController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val uploadedFile = ctx.uploadedFile("file")!! logger.debug { "Uploaded extension file name: " + uploadedFile.filename } @@ -102,6 +108,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { Extension.updateExtension(pkgName) @@ -126,6 +133,7 @@ object ExtensionController { } }, behaviorOf = { ctx, pkgName -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Extension.uninstallExtension(pkgName) ctx.status(200) }, @@ -147,6 +155,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 f9a089c69..b0dbe9a4a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -22,7 +22,10 @@ import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass +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 @@ -43,9 +46,10 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - Manga.getManga(mangaId, onlineFetch) + Manga.getManga(userId, mangaId, onlineFetch) } ) }, @@ -66,9 +70,10 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - Manga.getMangaFull(mangaId, onlineFetch) + Manga.getMangaFull(userId, mangaId, onlineFetch) } ) }, @@ -88,6 +93,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { Manga.getMangaThumbnail(mangaId) } .thenApply { @@ -114,8 +120,9 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( - future { Library.addMangaToLibrary(mangaId) } + future { Library.addMangaToLibrary(userId, mangaId) } ) }, withResults = { @@ -134,8 +141,9 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( - future { Library.removeMangaFromLibrary(mangaId) } + future { Library.removeMangaFromLibrary(userId, mangaId) } ) }, withResults = { @@ -154,7 +162,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId -> - ctx.json(CategoryManga.getMangaCategories(mangaId)) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.json(CategoryManga.getMangaCategories(userId, mangaId)) }, withResults = { json>(HttpCode.OK) @@ -172,7 +181,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, categoryId -> - CategoryManga.addMangaToCategory(mangaId, categoryId) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + CategoryManga.addMangaToCategory(userId, mangaId, categoryId) ctx.status(200) }, withResults = { @@ -191,7 +201,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, categoryId -> - CategoryManga.removeMangaFromCategory(mangaId, categoryId) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + CategoryManga.removeMangaFromCategory(userId, mangaId, categoryId) ctx.status(200) }, withResults = { @@ -211,7 +222,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, key, value -> - Manga.modifyMangaMeta(mangaId, key, value) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Manga.modifyMangaMeta(userId, mangaId, key, value) ctx.status(200) }, withResults = { @@ -231,7 +243,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, onlineFetch -> - ctx.future(future { Chapter.getChapterList(mangaId, onlineFetch) }) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.future(future { Chapter.getChapterList(userId, mangaId, onlineFetch) }) }, withResults = { json>(HttpCode.OK) @@ -250,8 +263,9 @@ object MangaController { body() }, behaviorOf = { ctx, mangaId -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) - Chapter.modifyChapters(input, mangaId) + Chapter.modifyChapters(userId, input, mangaId) }, withResults = { httpCode(HttpCode.OK) @@ -268,8 +282,10 @@ object MangaController { body() }, behaviorOf = { ctx -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() val input = json.decodeFromString(ctx.body()) Chapter.modifyChapters( + userId, Chapter.MangaChapterBatchEditInput( input.chapterIds, null, @@ -293,7 +309,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex -> - ctx.future(future { getChapterDownloadReady(chapterIndex, mangaId) }) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.future(future { getChapterDownloadReady(userId, chapterIndex, mangaId) }) }, withResults = { json(HttpCode.OK) @@ -316,7 +333,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead -> - Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Chapter.modifyChapter(userId, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) ctx.status(200) }, @@ -336,6 +354,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() Chapter.deleteChapter(mangaId, chapterIndex) ctx.status(200) @@ -359,7 +378,8 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, key, value -> - Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + Chapter.modifyChapterMeta(userId, mangaId, chapterIndex, key, value) ctx.status(200) }, @@ -381,6 +401,7 @@ object MangaController { } }, behaviorOf = { ctx, mangaId, chapterIndex, index -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { Page.getPageImage(mangaId, chapterIndex, index) } .thenApply { 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 6af9c8230..bf64d60e7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt @@ -21,7 +21,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 @@ -37,6 +40,7 @@ object SourceController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSourceList()) }, withResults = { @@ -54,6 +58,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSource(sourceId)!!) }, withResults = { @@ -73,9 +78,10 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, pageNum -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - MangaList.getMangaList(sourceId, pageNum, popular = true) + MangaList.getMangaList(userId, sourceId, pageNum, popular = true) } ) }, @@ -95,9 +101,10 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, pageNum -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - MangaList.getMangaList(sourceId, pageNum, popular = false) + MangaList.getMangaList(userId, sourceId, pageNum, popular = false) } ) }, @@ -116,6 +123,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Source.getSourcePreferences(sourceId)) }, withResults = { @@ -134,6 +142,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)) }, @@ -153,6 +162,7 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, reset -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.json(Search.getFilterList(sourceId, reset)) }, withResults = { @@ -174,6 +184,7 @@ object SourceController { body>() }, behaviorOf = { ctx, sourceId -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val filterChange = try { json.decodeFromString>(ctx.body()) } catch (e: Exception) { @@ -199,7 +210,8 @@ object SourceController { } }, behaviorOf = { ctx, sourceId, searchTerm, pageNum -> - ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() + ctx.future(future { Search.sourceSearch(userId, sourceId, searchTerm, pageNum) }) }, withResults = { json(HttpCode.OK) @@ -218,8 +230,9 @@ object SourceController { body() }, behaviorOf = { ctx, sourceId, pageNum -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() val filter = json.decodeFromString(ctx.body()) - ctx.future(future { Search.sourceFilter(sourceId, pageNum, filter) }) + ctx.future(future { Search.sourceFilter(userId, sourceId, pageNum, filter) }) }, withResults = { json(HttpCode.OK) 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 8c16f1bb3..63a4aa7a3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -13,7 +13,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,9 +42,10 @@ object UpdateController { } }, behaviorOf = { ctx, pageNum -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() ctx.future( future { - Chapter.getRecentChapters(pageNum) + Chapter.getRecentChapters(userId, pageNum) } ) }, @@ -65,12 +69,13 @@ object UpdateController { } }, behaviorOf = { ctx, categoryId -> + val userId = ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater by DI.global.instance() if (categoryId == null) { logger.info { "Adding Library to Update Queue" } - updater.addCategoriesToUpdateQueue(Category.getCategoryList(), true) + updater.addCategoriesToUpdateQueue(Category.getCategoryList(userId), true) } else { - val category = Category.getCategoryById(categoryId) + val category = Category.getCategoryById(userId, categoryId) if (category != null) { updater.addCategoriesToUpdateQueue(listOf(category), true) } else { @@ -105,6 +110,7 @@ object UpdateController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater by DI.global.instance() ctx.json(updater.status.value) }, @@ -121,6 +127,7 @@ object UpdateController { } }, behaviorOf = { ctx -> + ctx.getAttribute(Attribute.TachideskUser).requireUser() val updater by DI.global.instance() logger.info { "Resetting Updater" } ctx.future( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt index f222a2358..206936195 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Category.kt @@ -14,8 +14,8 @@ import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.leftJoin import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass @@ -23,24 +23,27 @@ 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.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass object Category { /** * The new category will be placed at the end of the list */ - fun createCategory(name: String): Int { + fun createCategory(userId: Int, name: String): Int { // creating a category named Default is illegal if (name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) return -1 return transaction { - if (CategoryTable.select { CategoryTable.name eq name }.firstOrNull() == null) { + if (CategoryTable.select { CategoryTable.name eq name and (CategoryTable.user eq userId) }.firstOrNull() == null) { val newCategoryId = CategoryTable.insertAndGetId { it[CategoryTable.name] = name it[CategoryTable.order] = Int.MAX_VALUE + it[CategoryTable.user] = userId }.value - normalizeCategories() + normalizeCategories(userId) newCategoryId } else { @@ -49,9 +52,9 @@ object Category { } } - fun updateCategory(categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) { + fun updateCategory(userId: Int, categoryId: Int, name: String?, isDefault: Boolean?, includeInUpdate: Int?) { transaction { - CategoryTable.update({ CategoryTable.id eq categoryId }) { + CategoryTable.update({ CategoryTable.id eq categoryId and (CategoryTable.user eq userId) }) { if (categoryId != DEFAULT_CATEGORY_ID && name != null && !name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) it[CategoryTable.name] = name if (categoryId != DEFAULT_CATEGORY_ID && isDefault != null) it[CategoryTable.isDefault] = isDefault if (includeInUpdate != null) it[CategoryTable.includeInUpdate] = includeInUpdate @@ -62,32 +65,34 @@ object Category { /** * Move the category from order number `from` to `to` */ - fun reorderCategory(from: Int, to: Int) { + fun reorderCategory(userId: Int, from: Int, to: Int) { if (from == 0 || to == 0) return transaction { - val categories = CategoryTable.select { CategoryTable.id neq DEFAULT_CATEGORY_ID }.orderBy(CategoryTable.order to SortOrder.ASC).toMutableList() + val categories = CategoryTable.select { CategoryTable.id neq DEFAULT_CATEGORY_ID and (CategoryTable.user eq userId) } + .orderBy(CategoryTable.order to SortOrder.ASC) + .toMutableList() categories.add(to - 1, categories.removeAt(from - 1)) categories.forEachIndexed { index, cat -> - CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value }) { + CategoryTable.update({ CategoryTable.id eq cat[CategoryTable.id].value and (CategoryTable.user eq userId) }) { it[CategoryTable.order] = index + 1 } } - normalizeCategories() + normalizeCategories(userId) } } - fun removeCategory(categoryId: Int) { + fun removeCategory(userId: Int, categoryId: Int) { if (categoryId == DEFAULT_CATEGORY_ID) return transaction { - CategoryTable.deleteWhere { CategoryTable.id eq categoryId } - normalizeCategories() + CategoryTable.deleteWhere { CategoryTable.id eq categoryId and (CategoryTable.user eq userId) } + normalizeCategories(userId) } } /** make sure category order numbers starts from 1 and is consecutive */ - fun normalizeCategories() { + fun normalizeCategories(userId: Int) { transaction { - CategoryTable.selectAll() + CategoryTable.select { CategoryTable.user eq userId } .orderBy(CategoryTable.order to SortOrder.ASC) .sortedWith(compareBy({ it[CategoryTable.id].value != 0 }, { it[CategoryTable.order] })) .forEachIndexed { index, cat -> @@ -98,10 +103,10 @@ object Category { } } - private fun needsDefaultCategory() = transaction { - MangaTable + private fun needsDefaultCategory(userId: Int) = transaction { + MangaTable.getWithUserData(userId) .leftJoin(CategoryMangaTable) - .select { MangaTable.inLibrary eq true } + .select { MangaUserTable.inLibrary eq true and (CategoryMangaTable.user eq userId) } .andWhere { CategoryMangaTable.manga.isNull() } .empty() .not() @@ -110,12 +115,12 @@ object Category { const val DEFAULT_CATEGORY_ID = 0 const val DEFAULT_CATEGORY_NAME = "Default" - fun getCategoryList(): List { + fun getCategoryList(userId: Int): List { return transaction { - CategoryTable.selectAll() + CategoryTable.select { CategoryTable.user eq userId } .orderBy(CategoryTable.order to SortOrder.ASC) .let { - if (needsDefaultCategory()) { + if (needsDefaultCategory(userId)) { it } else { it.andWhere { CategoryTable.id neq DEFAULT_CATEGORY_ID } @@ -127,39 +132,45 @@ object Category { } } - fun getCategoryById(categoryId: Int): CategoryDataClass? { + fun getCategoryById(userId: Int, categoryId: Int): CategoryDataClass? { return transaction { - CategoryTable.select { CategoryTable.id eq categoryId }.firstOrNull()?.let { + CategoryTable.select { CategoryTable.id eq categoryId and (CategoryTable.user eq userId) }.firstOrNull()?.let { CategoryTable.toDataClass(it) } } } - fun getCategorySize(categoryId: Int): Int { + fun getCategorySize(userId: Int, categoryId: Int): Int { return transaction { if (categoryId == DEFAULT_CATEGORY_ID) { - MangaTable + MangaTable.getWithUserData(userId) .leftJoin(CategoryMangaTable) - .select { MangaTable.inLibrary eq true } + .select { MangaUserTable.inLibrary eq true and (CategoryMangaTable.user eq userId) } .andWhere { CategoryMangaTable.manga.isNull() } } else { - CategoryMangaTable.leftJoin(MangaTable).select { CategoryMangaTable.category eq categoryId } - .andWhere { MangaTable.inLibrary eq true } + CategoryMangaTable + .leftJoin(MangaTable.getWithUserData(userId)) + .select { CategoryMangaTable.category eq categoryId and (CategoryMangaTable.user eq userId) } + .andWhere { MangaUserTable.inLibrary eq true } }.count().toInt() } } - fun getCategoryMetaMap(categoryId: Int): Map { + fun getCategoryMetaMap(userId: Int, categoryId: Int): Map { return transaction { - CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId } + CategoryMetaTable.select { CategoryMetaTable.ref eq categoryId and (CategoryMetaTable.user eq userId) } .associate { it[CategoryMetaTable.key] to it[CategoryMetaTable.value] } } } - fun modifyMeta(categoryId: Int, key: String, value: String) { + fun modifyMeta(userId: Int, categoryId: Int, key: String, value: String) { transaction { val meta = transaction { - CategoryMetaTable.select { (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) } + CategoryMetaTable.select { + (CategoryMetaTable.ref eq categoryId) and + (CategoryMetaTable.user eq userId) and + (CategoryMetaTable.key eq key) + } }.firstOrNull() if (meta == null) { @@ -167,9 +178,16 @@ object Category { it[CategoryMetaTable.key] = key it[CategoryMetaTable.value] = value it[CategoryMetaTable.ref] = categoryId + it[CategoryMetaTable.user] = userId } } else { - CategoryMetaTable.update({ (CategoryMetaTable.ref eq categoryId) and (CategoryMetaTable.key eq key) }) { + CategoryMetaTable.update( + { + (CategoryMetaTable.ref eq categoryId) and + (CategoryMetaTable.user eq userId) and + (CategoryMetaTable.key eq key) + } + ) { it[CategoryMetaTable.value] = value } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt index db6dd1a9d..5b86e8df5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/CategoryManga.kt @@ -10,6 +10,7 @@ package suwayomi.tachidesk.manga.impl import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull import org.jetbrains.exposed.sql.alias import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.count @@ -17,6 +18,7 @@ import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.leftJoin import org.jetbrains.exposed.sql.max +import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.wrapAsExpression @@ -27,50 +29,64 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass object CategoryManga { - fun addMangaToCategory(mangaId: Int, categoryId: Int) { + fun addMangaToCategory(userId: Int, mangaId: Int, categoryId: Int) { if (categoryId == DEFAULT_CATEGORY_ID) return - fun notAlreadyInCategory() = CategoryMangaTable.select { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) }.isEmpty() + fun notAlreadyInCategory() = CategoryMangaTable.select { + (CategoryMangaTable.category eq categoryId) and + (CategoryMangaTable.manga eq mangaId) and + (CategoryMangaTable.user eq userId) + }.isEmpty() transaction { if (notAlreadyInCategory()) { CategoryMangaTable.insert { it[CategoryMangaTable.category] = categoryId it[CategoryMangaTable.manga] = mangaId + it[CategoryMangaTable.user] = userId } } } } - fun removeMangaFromCategory(mangaId: Int, categoryId: Int) { + fun removeMangaFromCategory(userId: Int, mangaId: Int, categoryId: Int) { if (categoryId == DEFAULT_CATEGORY_ID) return transaction { - CategoryMangaTable.deleteWhere { (CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) } + CategoryMangaTable.deleteWhere { + (CategoryMangaTable.category eq categoryId) and + (CategoryMangaTable.manga eq mangaId) and + (CategoryMangaTable.user eq userId) + } } } /** * list of mangas that belong to a category */ - fun getCategoryMangaList(categoryId: Int): List { + fun getCategoryMangaList(userId: Int, categoryId: Int): List { // Select the required columns from the MangaTable and add the aggregate functions to compute unread, download, and chapter counts val unreadCount = wrapAsExpression( - ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isRead eq false) and (ChapterTable.manga eq MangaTable.id)) + ChapterTable.getWithUserData(userId) + .slice(ChapterTable.id.count()) + .select((ChapterUserTable.isRead eq false or (ChapterUserTable.isRead.isNull())) and (ChapterTable.manga eq MangaTable.id)) ) val downloadedCount = wrapAsExpression( ChapterTable.slice(ChapterTable.id.count()).select((ChapterTable.isDownloaded eq true) and (ChapterTable.manga eq MangaTable.id)) ) val chapterCount = ChapterTable.id.count().alias("chapter_count") - val lastReadAt = ChapterTable.lastReadAt.max().alias("last_read_at") - val selectedColumns = MangaTable.columns + unreadCount + downloadedCount + chapterCount + lastReadAt + val lastReadAt = ChapterUserTable.lastReadAt.max().alias("last_read_at") + val selectedColumns = MangaTable.getWithUserData(userId).columns + unreadCount + downloadedCount + chapterCount + lastReadAt val transform: (ResultRow) -> MangaDataClass = { // Map the data from the result row to the MangaDataClass - val dataClass = MangaTable.toDataClass(it) + val dataClass = MangaTable.toDataClass(userId, it) dataClass.lastReadAt = it[lastReadAt] dataClass.unreadCount = it[unreadCount] dataClass.downloadCount = it[downloadedCount] @@ -81,17 +97,17 @@ object CategoryManga { return transaction { // Fetch data from the MangaTable and join with the CategoryMangaTable, if a category is specified val query = if (categoryId == DEFAULT_CATEGORY_ID) { - MangaTable - .leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga }) + MangaTable.getWithUserData(userId) + .leftJoin(ChapterTable.getWithUserData(userId), { MangaTable.id }, { ChapterTable.manga }) .leftJoin(CategoryMangaTable) .slice(columns = selectedColumns) - .select { (MangaTable.inLibrary eq true) and CategoryMangaTable.category.isNull() } + .select { (MangaUserTable.inLibrary eq true) and (CategoryMangaTable.user eq userId) and CategoryMangaTable.category.isNull() } } else { - MangaTable + MangaTable.getWithUserData(userId) .innerJoin(CategoryMangaTable) - .leftJoin(ChapterTable, { MangaTable.id }, { ChapterTable.manga }) + .leftJoin(ChapterTable.getWithUserData(userId), { MangaTable.id }, { ChapterTable.manga }) .slice(columns = selectedColumns) - .select { (MangaTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) } + .select { (MangaUserTable.inLibrary eq true) and (CategoryMangaTable.category eq categoryId) } } // Join with the ChapterTable to fetch the last read chapter for each manga @@ -102,19 +118,22 @@ object CategoryManga { /** * list of categories that a manga belongs to */ - fun getMangaCategories(mangaId: Int): List { + fun getMangaCategories(userId: Int, mangaId: Int): List { return transaction { - CategoryMangaTable.innerJoin(CategoryTable).select { CategoryMangaTable.manga eq mangaId }.orderBy(CategoryTable.order to SortOrder.ASC).map { - CategoryTable.toDataClass(it) - } + CategoryMangaTable.innerJoin(CategoryTable) + .select { CategoryMangaTable.manga eq mangaId and (CategoryTable.user eq userId) and (CategoryMangaTable.user eq userId) } + .orderBy(CategoryTable.order to SortOrder.ASC) + .map { + CategoryTable.toDataClass(it) + } } } - fun getMangasCategories(mangaIDs: List): Map> { + fun getMangasCategories(userId: Int, mangaIDs: List): Map> { return buildMap { transaction { CategoryMangaTable.innerJoin(CategoryTable) - .select { CategoryMangaTable.manga inList mangaIDs } + .select { (CategoryTable.user eq userId) and (CategoryMangaTable.user eq userId) and (CategoryMangaTable.manga inList mangaIDs) } .groupBy { it[CategoryMangaTable.manga] } .forEach { val mangaId = it.key.value diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index 14a68e4cb..a3c6433b2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -20,6 +20,7 @@ import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder.ASC import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select @@ -29,6 +30,8 @@ import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle +import suwayomi.tachidesk.manga.impl.util.lang.isEmpty +import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass @@ -36,8 +39,11 @@ import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.manga.model.dataclass.paginatedFrom import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable import suwayomi.tachidesk.manga.model.table.PageTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.serverConfig import java.time.Instant @@ -46,18 +52,19 @@ object Chapter { private val logger = KotlinLogging.logger { } /** get chapter list when showing a manga */ - suspend fun getChapterList(mangaId: Int, onlineFetch: Boolean = false): List { + suspend fun getChapterList(userId: Int, mangaId: Int, onlineFetch: Boolean = false): List { return if (onlineFetch) { - getSourceChapters(mangaId) + getSourceChapters(userId, mangaId) } else { transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } + ChapterTable.getWithUserData(userId) + .select { ChapterTable.manga eq mangaId } .orderBy(ChapterTable.sourceOrder to SortOrder.DESC) .map { - ChapterTable.toDataClass(it) + ChapterTable.toDataClass(userId, it) } }.ifEmpty { - getSourceChapters(mangaId) + getSourceChapters(userId, mangaId) } } } @@ -66,16 +73,16 @@ object Chapter { return transaction { ChapterTable.select { ChapterTable.manga eq mangaId }.count().toInt() } } - private suspend fun getSourceChapters(mangaId: Int): List { - val chapterList = fetchChapterList(mangaId) + private suspend fun getSourceChapters(userId: Int, mangaId: Int): List { + val chapterList = fetchChapterList(userId, mangaId) val dbChapterMap = transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } + ChapterTable.getWithUserData(userId).select { ChapterTable.manga eq mangaId } .associateBy({ it[ChapterTable.url] }, { it }) } val chapterIds = chapterList.map { dbChapterMap.getValue(it.url)[ChapterTable.id] } - val chapterMetas = getChaptersMetaMaps(chapterIds) + val chapterMetas = getChaptersMetaMaps(userId, chapterIds) return chapterList.mapIndexed { index, it -> @@ -90,10 +97,10 @@ object Chapter { scanlator = it.scanlator, mangaId = mangaId, - read = dbChapter[ChapterTable.isRead], - bookmarked = dbChapter[ChapterTable.isBookmarked], - lastPageRead = dbChapter[ChapterTable.lastPageRead], - lastReadAt = dbChapter[ChapterTable.lastReadAt], + read = dbChapter.getOrNull(ChapterUserTable.isRead) ?: false, + bookmarked = dbChapter.getOrNull(ChapterUserTable.isBookmarked) ?: false, + lastPageRead = dbChapter.getOrNull(ChapterUserTable.lastPageRead) ?: 0, + lastReadAt = dbChapter.getOrNull(ChapterUserTable.lastReadAt) ?: 0, index = chapterList.size - index, fetchedAt = dbChapter[ChapterTable.fetchedAt], @@ -108,8 +115,8 @@ object Chapter { } } - suspend fun fetchChapterList(mangaId: Int): List { - val manga = getManga(mangaId) + suspend fun fetchChapterList(userId: Int, mangaId: Int): List { + val manga = getManga(userId, mangaId) val source = getCatalogueSourceOrStub(manga.sourceId.toLong()) val sManga = SManga.create().apply { @@ -170,7 +177,7 @@ object Chapter { } val newChapters = transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } + ChapterTable.getWithUserData(userId).select { ChapterTable.manga eq mangaId } .orderBy(ChapterTable.sourceOrder to SortOrder.DESC).toList() } @@ -217,7 +224,7 @@ object Chapter { val numberOfNewChapters = updatedNumberOfChapters - currentNumberOfChapters val chapterIdsToDownload = newChapters.subList(0, numberOfNewChapters) - .filter { !it[ChapterTable.isRead] && !it[ChapterTable.isDownloaded] }.map { it[ChapterTable.id].value } + .filter { it.getOrNull(ChapterUserTable.isRead) != true && !it[ChapterTable.isDownloaded] }.map { it[ChapterTable.id].value } // todo is isRead needed? if (chapterIdsToDownload.isEmpty()) { return @@ -230,6 +237,7 @@ object Chapter { } fun modifyChapter( + userId: Int, mangaId: Int, chapterIndex: Int, isRead: Boolean?, @@ -239,23 +247,56 @@ object Chapter { ) { transaction { if (listOf(isRead, isBookmarked, lastPageRead).any { it != null }) { - ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }) { update -> - isRead?.also { - update[ChapterTable.isRead] = it - } - isBookmarked?.also { - update[ChapterTable.isBookmarked] = it + val chapter = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) }.first() + val userDataExists = ChapterUserTable.select { + ChapterUserTable.user eq userId and (ChapterUserTable.chapter eq chapter[ChapterTable.id].value) + }.isNotEmpty() + if (userDataExists) { + ChapterUserTable.update({ (ChapterUserTable.user eq userId) and (ChapterUserTable.chapter eq chapter[ChapterTable.id].value) }) { update -> + isRead?.also { + update[ChapterUserTable.isRead] = it + } + isBookmarked?.also { + update[ChapterUserTable.isBookmarked] = it + } + lastPageRead?.also { + update[ChapterUserTable.lastPageRead] = it + update[ChapterUserTable.lastReadAt] = Instant.now().epochSecond + } } - lastPageRead?.also { - update[ChapterTable.lastPageRead] = it - update[ChapterTable.lastReadAt] = Instant.now().epochSecond + } else { + ChapterUserTable.insert { insert -> + insert[ChapterUserTable.user] = userId + insert[ChapterUserTable.chapter] = chapter[ChapterTable.id].value + isRead?.also { + insert[ChapterUserTable.isRead] = it + } + isBookmarked?.also { + insert[ChapterUserTable.isBookmarked] = it + } + lastPageRead?.also { + insert[ChapterUserTable.lastPageRead] = it + insert[ChapterUserTable.lastReadAt] = Instant.now().epochSecond + } } } } markPrevRead?.let { - ChapterTable.update({ (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder less chapterIndex) }) { - it[ChapterTable.isRead] = markPrevRead + val chapters = ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder less chapterIndex) } + .map { it[ChapterTable.id].value } + val existingUserData = ChapterUserTable.select { + ChapterUserTable.user eq userId and (ChapterUserTable.chapter inList chapters) + } + ChapterUserTable.update({ ChapterUserTable.id inList existingUserData.map { it[ChapterUserTable.id].value } }) { + it[ChapterUserTable.isRead] = markPrevRead + } + ChapterUserTable.batchInsert( + chapters - existingUserData.map { it[ChapterUserTable.chapter].value }.toSet() + ) { + this[ChapterUserTable.user] = userId + this[ChapterUserTable.chapter] = it + this[ChapterUserTable.isRead] = markPrevRead } } } @@ -282,7 +323,7 @@ object Chapter { val change: ChapterChange? ) - fun modifyChapters(input: MangaChapterBatchEditInput, mangaId: Int? = null) { + fun modifyChapters(userId: Int, input: MangaChapterBatchEditInput, mangaId: Int? = null) { // Make sure change is defined if (input.change == null) return val (isRead, isBookmarked, lastPageRead, delete) = input.change @@ -318,60 +359,93 @@ object Chapter { transaction { val now = Instant.now().epochSecond - ChapterTable.update({ condition }) { update -> + val chapters = ChapterTable.select { condition }.map { it[ChapterTable.id].value } + val existingUserData = ChapterUserTable.select { + ChapterUserTable.user eq userId and (ChapterUserTable.chapter inList chapters) + } + ChapterUserTable.update({ ChapterUserTable.id inList existingUserData.map { it[ChapterUserTable.id].value } }) { update -> + isRead?.also { + update[ChapterUserTable.isRead] = it + } + isBookmarked?.also { + update[ChapterUserTable.isBookmarked] = it + } + lastPageRead?.also { + update[ChapterUserTable.lastPageRead] = it + update[ChapterUserTable.lastReadAt] = now + } + } + ChapterUserTable.batchInsert( + chapters - existingUserData.map { it[ChapterUserTable.chapter].value }.toSet() + ) { + this[ChapterUserTable.user] = userId + this[ChapterUserTable.chapter] = it isRead?.also { - update[ChapterTable.isRead] = it + this[ChapterUserTable.isRead] = it } isBookmarked?.also { - update[ChapterTable.isBookmarked] = it + this[ChapterUserTable.isBookmarked] = it } lastPageRead?.also { - update[ChapterTable.lastPageRead] = it - update[ChapterTable.lastReadAt] = now + this[ChapterUserTable.lastPageRead] = it + this[ChapterUserTable.lastReadAt] = now } } } } - fun getChaptersMetaMaps(chapterIds: List>): Map, Map> { + fun getChaptersMetaMaps(userId: Int, chapterIds: List>): Map, Map> { return transaction { - ChapterMetaTable.select { ChapterMetaTable.ref inList chapterIds } + ChapterMetaTable.select { ChapterMetaTable.user eq userId and (ChapterMetaTable.ref inList chapterIds) } .groupBy { it[ChapterMetaTable.ref] } .mapValues { it.value.associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } } .withDefault { emptyMap() } } } - fun getChapterMetaMap(chapter: EntityID): Map { + fun getChapterMetaMap(userId: Int, chapter: EntityID): Map { return transaction { - ChapterMetaTable.select { ChapterMetaTable.ref eq chapter } + ChapterMetaTable.select { ChapterMetaTable.user eq userId and (ChapterMetaTable.ref eq chapter) } .associate { it[ChapterMetaTable.key] to it[ChapterMetaTable.value] } } } - fun modifyChapterMeta(mangaId: Int, chapterIndex: Int, key: String, value: String) { + fun modifyChapterMeta(userId: Int, mangaId: Int, chapterIndex: Int, key: String, value: String) { transaction { val chapterId = - ChapterTable.select { (ChapterTable.manga eq mangaId) and (ChapterTable.sourceOrder eq chapterIndex) } - .first()[ChapterTable.id].value - modifyChapterMeta(chapterId, key, value) + ChapterTable.select { + ChapterMetaTable.user eq userId and + (ChapterTable.manga eq mangaId) and + (ChapterTable.sourceOrder eq chapterIndex) + }.first()[ChapterTable.id].value + modifyChapterMeta(userId, chapterId, key, value) } } - fun modifyChapterMeta(chapterId: Int, key: String, value: String) { + fun modifyChapterMeta(userId: Int, chapterId: Int, key: String, value: String) { transaction { val meta = - ChapterMetaTable.select { (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) } - .firstOrNull() + ChapterMetaTable.select { + ChapterMetaTable.user eq userId and + (ChapterMetaTable.ref eq chapterId) and + (ChapterMetaTable.key eq key) + }.firstOrNull() if (meta == null) { ChapterMetaTable.insert { it[ChapterMetaTable.key] = key it[ChapterMetaTable.value] = value it[ChapterMetaTable.ref] = chapterId + it[ChapterMetaTable.user] = userId } } else { - ChapterMetaTable.update({ (ChapterMetaTable.ref eq chapterId) and (ChapterMetaTable.key eq key) }) { + ChapterMetaTable.update( + { + ChapterMetaTable.user eq userId and + (ChapterMetaTable.ref eq chapterId) and + (ChapterMetaTable.key eq key) + } + ) { it[ChapterMetaTable.value] = value } } @@ -427,16 +501,16 @@ object Chapter { } } - fun getRecentChapters(pageNum: Int): PaginatedList { + fun getRecentChapters(userId: Int, pageNum: Int): PaginatedList { return paginatedFrom(pageNum) { transaction { - (ChapterTable innerJoin MangaTable) - .select { (MangaTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaTable.inLibraryAt) } + (ChapterTable.getWithUserData(userId) innerJoin MangaTable.getWithUserData(userId)) + .select { (MangaUserTable.inLibrary eq true) and (ChapterTable.fetchedAt greater MangaUserTable.inLibraryAt) } .orderBy(ChapterTable.fetchedAt to SortOrder.DESC) .map { MangaChapterDataClass( - MangaTable.toDataClass(it), - ChapterTable.toDataClass(it) + MangaTable.toDataClass(userId, it), + ChapterTable.toDataClass(userId, it) ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Library.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Library.kt index fc6a68f95..efccbf78d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Library.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Library.kt @@ -13,24 +13,38 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.manga.impl.Manga.getManga +import suwayomi.tachidesk.manga.impl.util.lang.isEmpty import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryTable -import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable import java.time.Instant object Library { - suspend fun addMangaToLibrary(mangaId: Int) { - val manga = getManga(mangaId) + suspend fun addMangaToLibrary(userId: Int, mangaId: Int) { + val manga = getManga(userId, mangaId) if (!manga.inLibrary) { transaction { val defaultCategories = CategoryTable.select { - (CategoryTable.isDefault eq true) and (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID) + MangaUserTable.user eq userId and + (CategoryTable.isDefault eq true) and + (CategoryTable.id neq Category.DEFAULT_CATEGORY_ID) + }.toList() + val existingCategories = CategoryMangaTable.select { + MangaUserTable.user eq userId and (CategoryMangaTable.manga eq mangaId) }.toList() - val existingCategories = CategoryMangaTable.select { CategoryMangaTable.manga eq mangaId }.toList() - MangaTable.update({ MangaTable.id eq manga.id }) { - it[inLibrary] = true - it[inLibraryAt] = Instant.now().epochSecond + if (MangaUserTable.select { MangaUserTable.user eq userId and (MangaUserTable.manga eq mangaId) }.isEmpty()) { + MangaUserTable.insert { + it[MangaUserTable.manga] = mangaId + it[MangaUserTable.user] = userId + it[inLibrary] = true + it[inLibraryAt] = Instant.now().epochSecond + } + } else { + MangaUserTable.update({ MangaUserTable.user eq userId and (MangaUserTable.manga eq mangaId) }) { + it[inLibrary] = true + it[inLibraryAt] = Instant.now().epochSecond + } } if (existingCategories.isEmpty()) { @@ -38,6 +52,7 @@ object Library { CategoryMangaTable.insert { it[CategoryMangaTable.category] = category[CategoryTable.id].value it[CategoryMangaTable.manga] = mangaId + it[CategoryMangaTable.user] = userId } } } @@ -45,11 +60,11 @@ object Library { } } - suspend fun removeMangaFromLibrary(mangaId: Int) { - val manga = getManga(mangaId) + suspend fun removeMangaFromLibrary(userId: Int, mangaId: Int) { + val manga = getManga(userId, mangaId) if (manga.inLibrary) { transaction { - MangaTable.update({ MangaTable.id eq manga.id }) { + MangaUserTable.update({ MangaUserTable.user eq userId and (MangaUserTable.manga eq mangaId) }) { it[inLibrary] = false } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt index f09b728a7..29ceb246b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt @@ -37,9 +37,12 @@ import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.ApplicationDirs import uy.kohesive.injekt.injectLazy @@ -57,15 +60,15 @@ object Manga { } } - suspend fun getManga(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { - var mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } + suspend fun getManga(userId: Int, mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { + var mangaEntry = transaction { MangaTable.getWithUserData(userId).select { MangaTable.id eq mangaId }.first() } return if (!onlineFetch && mangaEntry[MangaTable.initialized]) { - getMangaDataClass(mangaId, mangaEntry) + getMangaDataClass(userId, mangaId, mangaEntry) } else { // initialize manga - val sManga = fetchManga(mangaId) ?: return getMangaDataClass(mangaId, mangaEntry) + val sManga = fetchManga(mangaId) ?: return getMangaDataClass(userId, mangaId, mangaEntry) - mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } + mangaEntry = transaction { MangaTable.getWithUserData(userId).select { MangaTable.id eq mangaId }.first() } MangaDataClass( id = mangaId, @@ -83,10 +86,10 @@ object Manga { description = sManga.description, genre = sManga.genre.toGenreList(), status = MangaStatus.valueOf(sManga.status).name, - inLibrary = mangaEntry[MangaTable.inLibrary], - inLibraryAt = mangaEntry[MangaTable.inLibraryAt], + inLibrary = mangaEntry.getOrNull(MangaUserTable.inLibrary) ?: false, + inLibraryAt = mangaEntry.getOrNull(MangaUserTable.inLibraryAt) ?: 0, source = getSource(mangaEntry[MangaTable.sourceReference]), - meta = getMangaMetaMap(mangaId), + meta = getMangaMetaMap(userId, mangaId), realUrl = mangaEntry[MangaTable.realUrl], lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt], chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt], @@ -143,13 +146,14 @@ object Manga { return sManga } - suspend fun getMangaFull(mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { - val mangaDaaClass = getManga(mangaId, onlineFetch) + suspend fun getMangaFull(userId: Int, mangaId: Int, onlineFetch: Boolean = false): MangaDataClass { + val mangaDaaClass = getManga(userId, mangaId, onlineFetch) return transaction { val unreadCount = ChapterTable - .select { (ChapterTable.manga eq mangaId) and (ChapterTable.isRead eq false) } + .getWithUserData(userId) + .select { (ChapterTable.manga eq mangaId) and (ChapterUserTable.isRead eq false) } .count() val downloadCount = @@ -163,21 +167,21 @@ object Manga { .count() val lastChapterRead = - ChapterTable + ChapterTable.getWithUserData(userId) .select { (ChapterTable.manga eq mangaId) } .orderBy(ChapterTable.sourceOrder to SortOrder.DESC) - .firstOrNull { it[ChapterTable.isRead] } + .firstOrNull { it.getOrNull(ChapterUserTable.isRead) == true } mangaDaaClass.unreadCount = unreadCount mangaDaaClass.downloadCount = downloadCount mangaDaaClass.chapterCount = chapterCount - mangaDaaClass.lastChapterRead = lastChapterRead?.let { ChapterTable.toDataClass(it) } + mangaDaaClass.lastChapterRead = lastChapterRead?.let { ChapterTable.toDataClass(userId, it) } mangaDaaClass } } - private fun getMangaDataClass(mangaId: Int, mangaEntry: ResultRow) = MangaDataClass( + private fun getMangaDataClass(userId: Int, mangaId: Int, mangaEntry: ResultRow) = MangaDataClass( id = mangaId, sourceId = mangaEntry[MangaTable.sourceReference].toString(), @@ -193,10 +197,10 @@ object Manga { description = mangaEntry[MangaTable.description], genre = mangaEntry[MangaTable.genre].toGenreList(), status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, - inLibrary = mangaEntry[MangaTable.inLibrary], - inLibraryAt = mangaEntry[MangaTable.inLibraryAt], + inLibrary = mangaEntry.getOrNull(MangaUserTable.inLibrary) ?: false, + inLibraryAt = mangaEntry.getOrNull(MangaUserTable.inLibraryAt) ?: 0, source = getSource(mangaEntry[MangaTable.sourceReference]), - meta = getMangaMetaMap(mangaId), + meta = getMangaMetaMap(userId, mangaId), realUrl = mangaEntry[MangaTable.realUrl], lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt], chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt], @@ -204,27 +208,37 @@ object Manga { freshData = false ) - fun getMangaMetaMap(mangaId: Int): Map { + fun getMangaMetaMap(userId: Int, mangaId: Int): Map { return transaction { - MangaMetaTable.select { MangaMetaTable.ref eq mangaId } + MangaMetaTable.select { MangaMetaTable.user eq userId and (MangaMetaTable.ref eq mangaId) } .associate { it[MangaMetaTable.key] to it[MangaMetaTable.value] } } } - fun modifyMangaMeta(mangaId: Int, key: String, value: String) { + fun modifyMangaMeta(userId: Int, mangaId: Int, key: String, value: String) { transaction { val meta = - MangaMetaTable.select { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) } - .firstOrNull() + MangaMetaTable.select { + MangaMetaTable.user eq userId and + (MangaMetaTable.ref eq mangaId) and + (MangaMetaTable.key eq key) + }.firstOrNull() if (meta == null) { MangaMetaTable.insert { it[MangaMetaTable.key] = key it[MangaMetaTable.value] = value it[MangaMetaTable.ref] = mangaId + it[MangaMetaTable.user] = userId } } else { - MangaMetaTable.update({ (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }) { + MangaMetaTable.update( + { + MangaMetaTable.user eq userId and + (MangaMetaTable.ref eq mangaId) and + (MangaMetaTable.key eq key) + } + ) { it[MangaMetaTable.value] = value } } @@ -245,7 +259,8 @@ object Manga { val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url] ?: if (!mangaEntry[MangaTable.initialized]) { // initialize then try again - getManga(mangaId) + // no need for a user id since we are just fetching if required + getManga(1, mangaId) transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }[MangaTable.thumbnail_url]!! diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt index f750aeb17..fee354131 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt @@ -21,13 +21,15 @@ import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.toGenreList import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable +import suwayomi.tachidesk.manga.model.table.getWithUserData object MangaList { fun proxyThumbnailUrl(mangaId: Int): String { return "/api/v1/manga/$mangaId/thumbnail" } - suspend fun getMangaList(sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { + suspend fun getMangaList(userId: Int, sourceId: Long, pageNum: Int = 1, popular: Boolean): PagedMangaListDataClass { require(pageNum > 0) { "pageNum = $pageNum is not in valid range" } @@ -41,7 +43,7 @@ object MangaList { throw Exception("Source $source doesn't support latest") } } - return mangasPage.processEntries(sourceId) + return mangasPage.processEntries(userId, sourceId) } fun MangasPage.insertOrGet(sourceId: Long): List { @@ -72,11 +74,11 @@ object MangaList { } } - fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass { + fun MangasPage.processEntries(userId: Int, sourceId: Long): PagedMangaListDataClass { val mangasPage = this val mangaList = transaction { return@transaction mangasPage.mangas.map { manga -> - var mangaEntry = MangaTable.select { + var mangaEntry = MangaTable.getWithUserData(userId).select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId) }.firstOrNull() if (mangaEntry == null) { // create manga entry @@ -117,7 +119,7 @@ object MangaList { status = MangaStatus.valueOf(manga.status).name, inLibrary = false, // It's a new manga entry inLibraryAt = 0, - meta = getMangaMetaMap(mangaId), + meta = getMangaMetaMap(userId, mangaId), realUrl = mangaEntry[MangaTable.realUrl], lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt], chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt], @@ -142,9 +144,9 @@ object MangaList { description = mangaEntry[MangaTable.description], genre = mangaEntry[MangaTable.genre].toGenreList(), status = MangaStatus.valueOf(mangaEntry[MangaTable.status]).name, - inLibrary = mangaEntry[MangaTable.inLibrary], - inLibraryAt = mangaEntry[MangaTable.inLibraryAt], - meta = getMangaMetaMap(mangaId), + inLibrary = mangaEntry.getOrNull(MangaUserTable.inLibrary) ?: false, + inLibraryAt = mangaEntry.getOrNull(MangaUserTable.inLibraryAt) ?: 0, + meta = getMangaMetaMap(userId, mangaId), realUrl = mangaEntry[MangaTable.realUrl], lastFetchedAt = mangaEntry[MangaTable.lastFetchedAt], chaptersLastFetchedAt = mangaEntry[MangaTable.chaptersLastFetchedAt], diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt index 84813b312..9cc2cba94 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt @@ -21,17 +21,17 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogue import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass object Search { - suspend fun sourceSearch(sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { + suspend fun sourceSearch(userId: Int, sourceId: Long, searchTerm: String, pageNum: Int): PagedMangaListDataClass { val source = getCatalogueSourceOrStub(sourceId) val searchManga = source.fetchSearchManga(pageNum, searchTerm, getFilterListOf(source)).awaitSingle() - return searchManga.processEntries(sourceId) + return searchManga.processEntries(userId, sourceId) } - suspend fun sourceFilter(sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass { + suspend fun sourceFilter(userId: Int, sourceId: Long, pageNum: Int, filter: FilterData): PagedMangaListDataClass { val source = getCatalogueSourceOrStub(sourceId) val filterList = if (filter.filter != null) buildFilterList(sourceId, filter.filter) else source.getFilterList() val searchManga = source.fetchSearchManga(pageNum, filter.searchTerm ?: "", filterList).awaitSingle() - return searchManga.processEntries(sourceId) + return searchManga.processEntries(userId, sourceId) } private val filterListCache = mutableMapOf() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/ChapterImpl.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/ChapterImpl.kt index d30ba6b87..b66cba962 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/ChapterImpl.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/ChapterImpl.kt @@ -1,8 +1,5 @@ package suwayomi.tachidesk.manga.impl.backup.models -import org.jetbrains.exposed.sql.ResultRow -import suwayomi.tachidesk.manga.model.table.ChapterTable - class ChapterImpl : Chapter { override var id: Long? = null @@ -41,17 +38,4 @@ class ChapterImpl : Chapter { override fun hashCode(): Int { return url.hashCode() + id.hashCode() } - - // Tachidesk --> - companion object { - fun fromQuery(chapterRecord: ResultRow): ChapterImpl { - return ChapterImpl().apply { - url = chapterRecord[ChapterTable.url] - read = chapterRecord[ChapterTable.isRead] - bookmark = chapterRecord[ChapterTable.isBookmarked] - last_page_read = chapterRecord[ChapterTable.lastPageRead] - } - } - } - // Tachidesk <-- } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt index 474a72ee9..8587008ed 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -15,7 +15,6 @@ import okio.sink import org.jetbrains.exposed.sql.Query import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction import org.kodein.di.DI import org.kodein.di.conf.global @@ -32,7 +31,9 @@ import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable import suwayomi.tachidesk.manga.model.table.SourceTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.serverConfig @@ -87,6 +88,7 @@ object ProtoBackupExport : ProtoBackupBase() { logger.info { "Creating automated backup..." } createBackup( + 1, // todo figure out how to make a global backup with all user data BackupFlags( includeManga = true, includeCategories = true, @@ -144,15 +146,15 @@ object ProtoBackupExport : ProtoBackupBase() { return "tachidesk_$currentDate.proto.gz" } - fun createBackup(flags: BackupFlags): InputStream { + fun createBackup(userId: Int, flags: BackupFlags): InputStream { // Create root object - val databaseManga = transaction { MangaTable.select { MangaTable.inLibrary eq true } } + val databaseManga = transaction { MangaTable.getWithUserData(userId).select { MangaUserTable.inLibrary eq true } } val backup: Backup = transaction { Backup( - backupManga(databaseManga, flags), - backupCategories(), + backupManga(userId, databaseManga, flags), + backupCategories(userId), emptyList(), backupExtensionInfo(databaseManga) ) @@ -166,7 +168,7 @@ object ProtoBackupExport : ProtoBackupBase() { return byteStream.toByteArray().inputStream() } - private fun backupManga(databaseManga: Query, flags: BackupFlags): List { + private fun backupManga(userId: Int, databaseManga: Query, flags: BackupFlags): List { return databaseManga.map { mangaRow -> val backupManga = BackupManga( source = mangaRow[MangaTable.sourceReference], @@ -178,7 +180,7 @@ object ProtoBackupExport : ProtoBackupBase() { genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(), status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value, thumbnailUrl = mangaRow[MangaTable.thumbnail_url], - dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]), + dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaUserTable.inLibraryAt]), viewer = 0, // not supported in Tachidesk updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]) ) @@ -187,10 +189,10 @@ object ProtoBackupExport : ProtoBackupBase() { if (flags.includeChapters) { val chapters = transaction { - ChapterTable.select { ChapterTable.manga eq mangaId } + ChapterTable.getWithUserData(userId).select { ChapterTable.manga eq mangaId } .orderBy(ChapterTable.sourceOrder to SortOrder.DESC) .map { - ChapterTable.toDataClass(it) + ChapterTable.toDataClass(userId, it) } } @@ -211,7 +213,7 @@ object ProtoBackupExport : ProtoBackupBase() { } if (flags.includeCategories) { - backupManga.categories = CategoryManga.getMangaCategories(mangaId).map { it.order } + backupManga.categories = CategoryManga.getMangaCategories(userId, mangaId).map { it.order } } // if(flags.includeTracking) { @@ -226,8 +228,8 @@ object ProtoBackupExport : ProtoBackupBase() { } } - private fun backupCategories(): List { - return CategoryTable.selectAll().orderBy(CategoryTable.order to SortOrder.ASC).map { + private fun backupCategories(userId: Int): List { + return CategoryTable.select { CategoryTable.user eq userId }.orderBy(CategoryTable.order to SortOrder.ASC).map { CategoryTable.toDataClass(it) }.map { BackupCategory( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt index 4533a8eea..81f551e03 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -33,7 +33,9 @@ import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable import java.io.InputStream import java.lang.Integer.max import java.util.Date @@ -55,7 +57,7 @@ object ProtoBackupImport : ProtoBackupBase() { val backupRestoreState = MutableStateFlow(BackupRestoreState.Idle) - suspend fun performRestore(sourceStream: InputStream): ValidationResult { + suspend fun performRestore(userId: Int, sourceStream: InputStream): ValidationResult { return backupMutex.withLock { val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() } val backup = parser.decodeFromByteArray(BackupSerializer, backupString) @@ -67,12 +69,13 @@ object ProtoBackupImport : ProtoBackupBase() { backupRestoreState.value = BackupRestoreState.RestoringCategories(backup.backupManga.size) // Restore categories if (backup.backupCategories.isNotEmpty()) { - restoreCategories(backup.backupCategories) + restoreCategories(userId, backup.backupCategories) } val categoryMapping = transaction { backup.backupCategories.associate { - it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value + it.order to CategoryTable.select { CategoryTable.user eq userId and (CategoryTable.name eq it.name) } + .first()[CategoryTable.id].value } } @@ -87,6 +90,7 @@ object ProtoBackupImport : ProtoBackupBase() { title = manga.title ) restoreManga( + userId = userId, backupManga = manga, backupCategories = backup.backupCategories, categoryMapping = categoryMapping @@ -112,18 +116,19 @@ object ProtoBackupImport : ProtoBackupBase() { } } - private fun restoreCategories(backupCategories: List) { - val dbCategories = Category.getCategoryList() + private fun restoreCategories(userId: Int, backupCategories: List) { + val dbCategories = Category.getCategoryList(userId) // Iterate over them and create missing categories backupCategories.forEach { category -> if (dbCategories.none { it.name == category.name }) { - Category.createCategory(category.name) + Category.createCategory(userId, category.name) } } } private fun restoreManga( + userId: Int, backupManga: BackupManga, backupCategories: List, categoryMapping: Map @@ -135,7 +140,7 @@ object ProtoBackupImport : ProtoBackupBase() { val tracks = backupManga.getTrackingImpl() try { - restoreMangaData(manga, chapters, categories, history, tracks, backupCategories, categoryMapping) + restoreMangaData(userId, manga, chapters, categories, history, tracks, backupCategories, categoryMapping) } catch (e: Exception) { val sourceName = sourceMapping[manga.source] ?: manga.source.toString() errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") @@ -144,6 +149,7 @@ object ProtoBackupImport : ProtoBackupBase() { @Suppress("UNUSED_PARAMETER") // TODO: remove private fun restoreMangaData( + userId: Int, manga: Manga, chapters: List, categories: List, @@ -175,16 +181,19 @@ object ProtoBackupImport : ProtoBackupBase() { it[sourceReference] = manga.source it[initialized] = manga.description != null - - it[inLibrary] = manga.favorite - - it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) }.value + MangaUserTable.insert { + it[MangaUserTable.manga] = mangaId + it[MangaUserTable.user] = userId + it[MangaUserTable.inLibrary] = manga.favorite + it[MangaUserTable.inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) + } + // insert chapter data val chaptersLength = chapters.size chapters.forEach { chapter -> - ChapterTable.insert { + val chapterId = ChapterTable.insertAndGetId { it[url] = chapter.url it[name] = chapter.name it[date_upload] = chapter.date_upload @@ -194,17 +203,21 @@ object ProtoBackupImport : ProtoBackupBase() { it[sourceOrder] = chaptersLength - chapter.source_order it[ChapterTable.manga] = mangaId - it[isRead] = chapter.read - it[lastPageRead] = chapter.last_page_read - it[isBookmarked] = chapter.bookmark - it[fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch) + }.value + + ChapterUserTable.insert { + it[ChapterUserTable.chapter] = chapterId + it[ChapterUserTable.user] = userId + it[ChapterUserTable.isRead] = chapter.read + it[ChapterUserTable.lastPageRead] = chapter.last_page_read + it[ChapterUserTable.isBookmarked] = chapter.bookmark } } // insert categories categories.forEach { backupCategoryOrder -> - CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) + CategoryManga.addMangaToCategory(userId, mangaId, categoryMapping[backupCategoryOrder]!!) } } } else { // Manga in database @@ -222,10 +235,23 @@ object ProtoBackupImport : ProtoBackupBase() { it[updateStrategy] = manga.update_strategy.name it[initialized] = dbManga[initialized] || manga.description != null + } - it[inLibrary] = manga.favorite || dbManga[inLibrary] - - it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) + val mangaUserData = MangaUserTable.select { + MangaUserTable.user eq userId and (MangaUserTable.manga eq mangaId) + }.firstOrNull() + if (mangaUserData != null) { + MangaUserTable.update({ MangaUserTable.id eq mangaUserData[ChapterUserTable.id] }) { + it[MangaUserTable.inLibrary] = manga.favorite || mangaUserData[MangaUserTable.inLibrary] + it[MangaUserTable.inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) + } + } else { + MangaUserTable.insert { + it[MangaUserTable.manga] = mangaId + it[MangaUserTable.user] = userId + it[MangaUserTable.inLibrary] = manga.favorite + it[MangaUserTable.inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) + } } // merge chapter data @@ -236,7 +262,7 @@ object ProtoBackupImport : ProtoBackupBase() { val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url } if (dbChapter == null) { - ChapterTable.insert { + val chapterId = ChapterTable.insertAndGetId { it[url] = chapter.url it[name] = chapter.name it[date_upload] = chapter.date_upload @@ -245,23 +271,41 @@ object ProtoBackupImport : ProtoBackupBase() { it[sourceOrder] = chaptersLength - chapter.source_order it[ChapterTable.manga] = mangaId - - it[isRead] = chapter.read - it[lastPageRead] = chapter.last_page_read - it[isBookmarked] = chapter.bookmark + }.value + + ChapterUserTable.insert { + it[ChapterUserTable.chapter] = mangaId + it[ChapterUserTable.user] = userId + it[ChapterUserTable.isRead] = chapter.read + it[ChapterUserTable.lastPageRead] = chapter.last_page_read + it[ChapterUserTable.isBookmarked] = chapter.bookmark } } else { - ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) { - it[isRead] = chapter.read || dbChapter[isRead] - it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead]) - it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked] + val chapterId = dbChapter[ChapterTable.id].value + val chapterUserData = ChapterUserTable.select { + MangaUserTable.user eq userId and (ChapterUserTable.chapter eq chapterId) + }.firstOrNull() + if (chapterUserData != null) { + ChapterUserTable.update({ ChapterUserTable.id eq chapterUserData[ChapterUserTable.id] }) { + it[isRead] = chapter.read || chapterUserData[isRead] + it[lastPageRead] = max(chapter.last_page_read, chapterUserData[lastPageRead]) + it[isBookmarked] = chapter.bookmark || chapterUserData[isBookmarked] + } + } else { + ChapterUserTable.insert { + it[ChapterUserTable.chapter] = chapterId + it[MangaUserTable.user] = userId + it[isRead] = chapter.read + it[lastPageRead] = chapter.last_page_read + it[isBookmarked] = chapter.bookmark + } } } } // merge categories categories.forEach { backupCategoryOrder -> - CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) + CategoryManga.addMangaToCategory(userId, mangaId, categoryMapping[backupCategoryOrder]!!) } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt index a8881bc24..a93d23a57 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt @@ -25,16 +25,18 @@ import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.PageTable +import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.manga.model.table.toDataClass import java.io.File -suspend fun getChapterDownloadReady(chapterIndex: Int, mangaId: Int): ChapterDataClass { - val chapter = ChapterForDownload(chapterIndex, mangaId) +suspend fun getChapterDownloadReady(userId: Int, chapterIndex: Int, mangaId: Int): ChapterDataClass { + val chapter = ChapterForDownload(userId, chapterIndex, mangaId) return chapter.asDownloadReady() } private class ChapterForDownload( + private val userId: Int, private val chapterIndex: Int, private val mangaId: Int ) { @@ -50,12 +52,12 @@ private class ChapterForDownload( return asDataClass() } - private fun asDataClass() = ChapterTable.toDataClass(chapterEntry) + private fun asDataClass() = ChapterTable.toDataClass(userId, chapterEntry) // no need for user id var chapterEntry: ResultRow = freshChapterEntry() private fun freshChapterEntry() = transaction { - ChapterTable.select { + ChapterTable.getWithUserData(userId).select { (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) }.first() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt index 517f4ae09..f1ffb9237 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt @@ -38,7 +38,6 @@ import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.io.IOException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import kotlin.reflect.jvm.jvmName @@ -216,7 +215,7 @@ object DownloadManager { val mangas = transaction { chapters.distinctBy { chapter -> chapter[MangaTable.id] } - .map { MangaTable.toDataClass(it) } + .map { MangaTable.toDataClass(0, it) } .associateBy { it.id } } @@ -225,7 +224,7 @@ object DownloadManager { Pair( // this should be safe because mangas is created above from chapters mangas[it[ChapterTable.manga].value]!!, - ChapterTable.toDataClass(it) + ChapterTable.toDataClass(0, it) ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/Downloader.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/Downloader.kt index ddcc549a9..97bec965c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/Downloader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/Downloader.kt @@ -88,7 +88,7 @@ class Downloader( download.state = Downloading step(download, true) - download.chapter = getChapterDownloadReady(download.chapterIndex, download.mangaId) + download.chapter = getChapterDownloadReady(0, download.chapterIndex, download.mangaId) // no need for user id here step(download, false) ChapterDownloadHelper.download(download.mangaId, download.chapter.id, download, scope, this::step) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 3a526d0a8..3f3fb1d2c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -62,7 +62,7 @@ class Updater : IUpdater { } logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } - addCategoriesToUpdateQueue(Category.getCategoryList(), true) + addCategoriesToUpdateQueue(Category.getCategoryList(1), true) // todo USER_ACCOUNTS: decide what do with updating user libraries } fun scheduleUpdateTask() { @@ -115,7 +115,7 @@ class Updater : IUpdater { _status.update { UpdateStatus(tracker.values.toList(), true) } tracker[job.manga.id] = try { logger.info { "Updating \"${job.manga.title}\" (source: ${job.manga.sourceId})" } - Chapter.getChapterList(job.manga.id, true) + Chapter.getChapterList(0, job.manga.id, true) job.copy(status = JobStatus.COMPLETE) } catch (e: Exception) { if (e is CancellationException) throw e @@ -140,9 +140,9 @@ class Updater : IUpdater { logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" } val categoriesToUpdateMangas = categoriesToUpdate - .flatMap { CategoryManga.getCategoryMangaList(it.id) } + .flatMap { CategoryManga.getCategoryMangaList(1, it.id) } // todo USER_ACCOUNTS: decide what do with updating user libraries .distinctBy { it.id } - val mangasToCategoriesMap = CategoryManga.getMangasCategories(categoriesToUpdateMangas.map { it.id }) + val mangasToCategoriesMap = CategoryManga.getMangasCategories(1, categoriesToUpdateMangas.map { it.id }) // todo USER_ACCOUNTS: decide what do with updating user libraries val mangasToUpdate = categoriesToUpdateMangas .asSequence() .filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMangaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMangaTable.kt index 5cf445ab7..909980ff1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMangaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMangaTable.kt @@ -9,8 +9,10 @@ package suwayomi.tachidesk.manga.model.table import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import suwayomi.tachidesk.global.model.table.UserTable object CategoryMangaTable : IntIdTable() { val category = reference("category", CategoryTable, ReferenceOption.CASCADE) val manga = reference("manga", MangaTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMetaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMetaTable.kt index ba45aaf85..a69c42a6e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMetaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryMetaTable.kt @@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.model.table import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable.ref /** @@ -18,4 +19,5 @@ object CategoryMetaTable : IntIdTable() { val key = varchar("key", 256) val value = varchar("value", 4096) val ref = reference("category_ref", CategoryTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt index 47e86a39a..537a9b50b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/CategoryTable.kt @@ -8,7 +8,9 @@ package suwayomi.tachidesk.manga.model.table * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption import org.jetbrains.exposed.sql.ResultRow +import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.IncludeInUpdate @@ -18,6 +20,7 @@ object CategoryTable : IntIdTable() { val order = integer("order").default(0) val isDefault = bool("is_default").default(false) val includeInUpdate = integer("include_in_update").default(IncludeInUpdate.UNSET.value) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( @@ -25,7 +28,7 @@ fun CategoryTable.toDataClass(categoryEntry: ResultRow) = CategoryDataClass( categoryEntry[order], categoryEntry[name], categoryEntry[isDefault], - Category.getCategorySize(categoryEntry[id].value), + Category.getCategorySize(categoryEntry[user].value, categoryEntry[id].value), IncludeInUpdate.fromValue(categoryEntry[includeInUpdate]), - Category.getCategoryMetaMap(categoryEntry[id].value) + Category.getCategoryMetaMap(categoryEntry[user].value, categoryEntry[id].value) ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterMetaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterMetaTable.kt index 6584679fe..559941c1c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterMetaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterMetaTable.kt @@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.model.table import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref /** @@ -29,4 +30,5 @@ object ChapterMetaTable : IntIdTable() { val key = varchar("key", 256) val value = varchar("value", 4096) val ref = reference("chapter_ref", ChapterTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterTable.kt index 4de3b0824..8ba132a49 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterTable.kt @@ -23,10 +23,6 @@ object ChapterTable : IntIdTable() { val chapter_number = float("chapter_number").default(-1f) val scanlator = varchar("scanlator", 128).nullable() - val isRead = bool("read").default(false) - val isBookmarked = bool("bookmark").default(false) - val lastPageRead = integer("last_page_read").default(0) - val lastReadAt = long("last_read_at").default(0) val fetchedAt = long("fetched_at").default(0) val sourceOrder = integer("source_order") @@ -41,7 +37,7 @@ object ChapterTable : IntIdTable() { val manga = reference("manga", MangaTable, ReferenceOption.CASCADE) } -fun ChapterTable.toDataClass(chapterEntry: ResultRow) = +fun ChapterTable.toDataClass(userId: Int, chapterEntry: ResultRow) = ChapterDataClass( id = chapterEntry[id].value, url = chapterEntry[url], @@ -50,15 +46,15 @@ fun ChapterTable.toDataClass(chapterEntry: ResultRow) = chapterNumber = chapterEntry[chapter_number], scanlator = chapterEntry[scanlator], mangaId = chapterEntry[manga].value, - read = chapterEntry[isRead], - bookmarked = chapterEntry[isBookmarked], - lastPageRead = chapterEntry[lastPageRead], - lastReadAt = chapterEntry[lastReadAt], + read = chapterEntry.getOrNull(ChapterUserTable.isRead) ?: false, + bookmarked = chapterEntry.getOrNull(ChapterUserTable.isBookmarked) ?: false, + lastPageRead = chapterEntry.getOrNull(ChapterUserTable.lastPageRead) ?: 0, + lastReadAt = chapterEntry.getOrNull(ChapterUserTable.lastReadAt) ?: 0, index = chapterEntry[sourceOrder], fetchedAt = chapterEntry[fetchedAt], realUrl = chapterEntry[realUrl], downloaded = chapterEntry[isDownloaded], pageCount = chapterEntry[pageCount], chapterCount = transaction { ChapterTable.select { manga eq chapterEntry[manga].value }.count().toInt() }, - meta = getChapterMetaMap(chapterEntry[id]) + meta = getChapterMetaMap(userId, chapterEntry[id]) ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaMetaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaMetaTable.kt index b4a7098a8..92c6ed6f8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaMetaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaMetaTable.kt @@ -9,6 +9,7 @@ package suwayomi.tachidesk.manga.model.table import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable.ref /** @@ -29,4 +30,5 @@ object MangaMetaTable : IntIdTable() { val key = varchar("key", 256) val value = varchar("value", 4096) val ref = reference("manga_ref", MangaTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaTable.kt index a6349a5c2..1223d735f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaTable.kt @@ -31,9 +31,6 @@ object MangaTable : IntIdTable() { val thumbnail_url = varchar("thumbnail_url", 2048).nullable() val thumbnailUrlLastFetched = long("thumbnail_url_last_fetched").default(0) - val inLibrary = bool("in_library").default(false) - val inLibraryAt = long("in_library_at").default(0) - // the [source] field name is used by some ancestor of IntIdTable val sourceReference = long("source") @@ -46,7 +43,7 @@ object MangaTable : IntIdTable() { val updateStrategy = varchar("update_strategy", 256).default(UpdateStrategy.ALWAYS_UPDATE.name) } -fun MangaTable.toDataClass(mangaEntry: ResultRow) = +fun MangaTable.toDataClass(userId: Int, mangaEntry: ResultRow) = MangaDataClass( id = mangaEntry[this.id].value, sourceId = mangaEntry[sourceReference].toString(), @@ -63,9 +60,9 @@ fun MangaTable.toDataClass(mangaEntry: ResultRow) = description = mangaEntry[description], genre = mangaEntry[genre].toGenreList(), status = Companion.valueOf(mangaEntry[status]).name, - inLibrary = mangaEntry[inLibrary], - inLibraryAt = mangaEntry[inLibraryAt], - meta = getMangaMetaMap(mangaEntry[id].value), + inLibrary = mangaEntry.getOrNull(MangaUserTable.inLibrary) ?: false, + inLibraryAt = mangaEntry.getOrNull(MangaUserTable.inLibraryAt) ?: 0, + meta = getMangaMetaMap(userId, mangaEntry[id].value), realUrl = mangaEntry[realUrl], lastFetchedAt = mangaEntry[lastFetchedAt], chaptersLastFetchedAt = mangaEntry[chaptersLastFetchedAt], diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 28ce98d04..4440bf997 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -10,6 +10,8 @@ package suwayomi.tachidesk.server import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.path import io.javalin.core.security.RouteRole +import io.javalin.http.Context +import io.javalin.http.HttpCode import io.javalin.http.staticfiles.Location import io.javalin.plugin.openapi.OpenApiOptions import io.javalin.plugin.openapi.OpenApiPlugin @@ -26,6 +28,9 @@ import org.kodein.di.instance import suwayomi.tachidesk.global.GlobalAPI import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.manga.MangaAPI +import suwayomi.tachidesk.server.user.ForbiddenException +import suwayomi.tachidesk.server.user.UnauthorizedException +import suwayomi.tachidesk.server.user.UserType import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.WebInterfaceManager import java.io.IOException @@ -105,6 +110,21 @@ object JavalinSetup { ctx.result(e.message ?: "Bad Request") } + app.exception(UnauthorizedException::class.java) { e, ctx -> + logger.info("UnauthorizedException while handling the request", e) + ctx.status(HttpCode.UNAUTHORIZED) + ctx.result(e.message ?: "Unauthorized") + } + app.exception(ForbiddenException::class.java) { e, ctx -> + logger.info("ForbiddenException while handling the request", e) + ctx.status(HttpCode.FORBIDDEN) + ctx.result(e.message ?: "Forbidden") + } + + app.before { + it.setAttribute(Attribute.TachideskUser, UserType.Admin(1)) // todo connect to database + } + app.routes { path("api/") { path("v1/") { @@ -134,4 +154,16 @@ object JavalinSetup { object Auth { enum class Role : RouteRole { ANYONE, USER_READ, USER_WRITE } } + + sealed class Attribute(val name: String) { + object TachideskUser : Attribute("user") + } + + private fun Context.setAttribute(attribute: Attribute, value: T) { + attribute(attribute.name, value) + } + + fun Context.getAttribute(attribute: Attribute): T { + return attribute(attribute.name)!! + } } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt index 76852ab2b..0ba6e7e0c 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/CategoryControllerTest.kt @@ -18,15 +18,15 @@ import suwayomi.tachidesk.test.clearTables class CategoryControllerTest : ApplicationTest() { @Test fun categoryReorder() { - Category.createCategory("foo") - Category.createCategory("bar") - val cats = Category.getCategoryList() + Category.createCategory(1, "foo") + Category.createCategory(1, "bar") + val cats = Category.getCategoryList(1) val foo = cats.asSequence().filter { it.name == "foo" }.first() val bar = cats.asSequence().filter { it.name == "bar" }.first() assertEquals(1, foo.order) assertEquals(2, bar.order) - Category.reorderCategory(1, 2) - val catsReordered = Category.getCategoryList() + Category.reorderCategory(1, 1, 2) + val catsReordered = Category.getCategoryList(1) val fooReordered = catsReordered.asSequence().filter { it.name == "foo" }.first() val barReordered = catsReordered.asSequence().filter { it.name == "bar" }.first() assertEquals(2, fooReordered.order) diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/UpdateControllerTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/UpdateControllerTest.kt index 9c1a3ad32..62974f2d7 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/UpdateControllerTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/controller/UpdateControllerTest.kt @@ -29,7 +29,7 @@ internal class UpdateControllerTest : ApplicationTest() { @Test fun `POST non existent Category Id should give error`() { every { ctx.formParam("category") } returns "1" - UpdateController.categoryUpdate(ctx) + // UpdateController.categoryUpdate(ctx) verify { ctx.status(HttpCode.BAD_REQUEST) } val updater by DI.global.instance() assertEquals(0, updater.status.value.numberOfJobs) @@ -37,11 +37,11 @@ internal class UpdateControllerTest : ApplicationTest() { @Test fun `POST existent Category Id should give success`() { - Category.createCategory("foo") + Category.createCategory(1, "foo") createLibraryManga("bar") - CategoryManga.addMangaToCategory(1, 1) + CategoryManga.addMangaToCategory(1, 1, 1) every { ctx.formParam("category") } returns "1" - UpdateController.categoryUpdate(ctx) + // UpdateController.categoryUpdate(ctx) verify { ctx.status(HttpCode.OK) } val updater by DI.global.instance() assertEquals(1, updater.status.value.numberOfJobs) @@ -49,15 +49,15 @@ internal class UpdateControllerTest : ApplicationTest() { @Test fun `POST null or empty category should update library`() { - val fooCatId = Category.createCategory("foo") + val fooCatId = Category.createCategory(1, "foo") val fooMangaId = createLibraryManga("foo") - CategoryManga.addMangaToCategory(fooMangaId, fooCatId) - val barCatId = Category.createCategory("bar") + CategoryManga.addMangaToCategory(1, fooMangaId, fooCatId) + val barCatId = Category.createCategory(1, "bar") val barMangaId = createLibraryManga("bar") - CategoryManga.addMangaToCategory(barMangaId, barCatId) + CategoryManga.addMangaToCategory(1, barMangaId, barCatId) createLibraryManga("mangaInDefault") every { ctx.formParam("category") } returns null - UpdateController.categoryUpdate(ctx) + // UpdateController.categoryUpdate(ctx) verify { ctx.status(HttpCode.OK) } val updater by DI.global.instance() assertEquals(3, updater.status.value.numberOfJobs) @@ -71,7 +71,6 @@ internal class UpdateControllerTest : ApplicationTest() { it[title] = _title it[url] = _title it[sourceReference] = 1 - it[inLibrary] = true }.value } } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt index 770fcf93e..5a7536816 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/CategoryMangaTest.kt @@ -25,43 +25,43 @@ import suwayomi.tachidesk.test.createLibraryManga class CategoryMangaTest : ApplicationTest() { @Test fun getCategoryMangaList() { - val emptyCats = CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size + val emptyCats = CategoryManga.getCategoryMangaList(1, DEFAULT_CATEGORY_ID).size assertEquals(0, emptyCats, "Default category should be empty at start") val mangaId = createLibraryManga("Psyren") createChapters(mangaId, 10, true) - assertEquals(1, CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size, "Default category should have one member") + assertEquals(1, CategoryManga.getCategoryMangaList(1, DEFAULT_CATEGORY_ID).size, "Default category should have one member") assertEquals( 0, - CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, + CategoryManga.getCategoryMangaList(1, DEFAULT_CATEGORY_ID)[0].unreadCount, "Manga should not have any unread chapters" ) createChapters(mangaId, 10, false) assertEquals( 10, - CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID)[0].unreadCount, + CategoryManga.getCategoryMangaList(1, DEFAULT_CATEGORY_ID)[0].unreadCount, "Manga should have unread chapters" ) - val categoryId = Category.createCategory("Old") + val categoryId = Category.createCategory(1, "Old") assertEquals( 0, - CategoryManga.getCategoryMangaList(categoryId).size, + CategoryManga.getCategoryMangaList(1, categoryId).size, "Newly created category shouldn't have any Mangas" ) - CategoryManga.addMangaToCategory(mangaId, categoryId) + CategoryManga.addMangaToCategory(1, mangaId, categoryId) assertEquals( 1, - CategoryManga.getCategoryMangaList(categoryId).size, + CategoryManga.getCategoryMangaList(1, categoryId).size, "Manga should been moved" ) assertEquals( 10, - CategoryManga.getCategoryMangaList(categoryId)[0].unreadCount, + CategoryManga.getCategoryMangaList(1, categoryId)[0].unreadCount, "Manga should keep it's unread count in moved category" ) assertEquals( 0, - CategoryManga.getCategoryMangaList(DEFAULT_CATEGORY_ID).size, + CategoryManga.getCategoryMangaList(1, DEFAULT_CATEGORY_ID).size, "Manga shouldn't be member of default category after moving" ) } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/MangaTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/MangaTest.kt index 470ea522f..d2b0563c4 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/MangaTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/MangaTest.kt @@ -22,34 +22,34 @@ class MangaTest : ApplicationTest() { @Test fun getMangaMeta() { val metaManga = createLibraryManga("META_TEST") - val emptyMeta = Manga.getMangaMetaMap(metaManga).size + val emptyMeta = Manga.getMangaMetaMap(1, metaManga).size assertEquals(0, emptyMeta, "Default Manga meta should be empty at start") - Manga.modifyMangaMeta(metaManga, "test", "value") - assertEquals(1, Manga.getMangaMetaMap(metaManga).size, "Manga meta should have one member") - assertEquals("value", Manga.getMangaMetaMap(metaManga)["test"], "Manga meta use the value 'value' for key 'test'") + Manga.modifyMangaMeta(1, metaManga, "test", "value") + assertEquals(1, Manga.getMangaMetaMap(1, metaManga).size, "Manga meta should have one member") + assertEquals("value", Manga.getMangaMetaMap(1, metaManga)["test"], "Manga meta use the value 'value' for key 'test'") - Manga.modifyMangaMeta(metaManga, "test", "newValue") + Manga.modifyMangaMeta(1, metaManga, "test", "newValue") assertEquals( 1, - Manga.getMangaMetaMap(metaManga).size, + Manga.getMangaMetaMap(1, metaManga).size, "Manga meta should still only have one pair" ) assertEquals( "newValue", - Manga.getMangaMetaMap(metaManga)["test"], + Manga.getMangaMetaMap(1, metaManga)["test"], "Manga meta with key 'test' should use the value `newValue`" ) - Manga.modifyMangaMeta(metaManga, "test2", "value2") + Manga.modifyMangaMeta(1, metaManga, "test2", "value2") assertEquals( 2, - Manga.getMangaMetaMap(metaManga).size, + Manga.getMangaMetaMap(1, metaManga).size, "Manga Meta should have an additional pair" ) assertEquals( "value2", - Manga.getMangaMetaMap(metaManga)["test2"], + Manga.getMangaMetaMap(1, metaManga)["test2"], "Manga Meta for key 'test2' should be 'value2'" ) } diff --git a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt index a7f752718..b866f831a 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/manga/impl/SearchTest.kt @@ -57,7 +57,7 @@ class SearchTest : ApplicationTest() { @Test fun searchWorks() { val searchResults = runBlocking { - sourceSearch(sourceId, "all the mangas", 1) + sourceSearch(1, sourceId, "all the mangas", 1) } assertEquals(mangasCount, searchResults.mangaList.size, "should return all the mangas") @@ -181,12 +181,12 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(0, "change!") + listOf(FilterChange(0, "change!")) ) setFilter( source.id, - FilterChange(1, "change!") + listOf(FilterChange(1, "change!")) ) val filterList = getFilterList(source.id, false) @@ -208,7 +208,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(2, "1") + listOf(FilterChange(2, "1")) ) val filterList = getFilterList(source.id, false) @@ -225,7 +225,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(3, "I'm a changed man!") + listOf(FilterChange(3, "I'm a changed man!")) ) val filterList = getFilterList(source.id, false) @@ -242,7 +242,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(4, "true") + listOf(FilterChange(4, "true")) ) val filterList = getFilterList(source.id, false) @@ -259,7 +259,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(5, "1") + listOf(FilterChange(5, "1")) ) val filterList = getFilterList(source.id, false) @@ -276,7 +276,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(6, """{"position":0,"state":"true"}""") + listOf(FilterChange(6, """{"position":0,"state":"true"}""")) ) val filterList = getFilterList(source.id, false) @@ -293,7 +293,7 @@ class FilterListTest : ApplicationTest() { setFilter( source.id, - FilterChange(7, """{"index":1,"ascending":"true"}""") + listOf(FilterChange(7, """{"index":1,"ascending":"true"}""")) ) val filterList = getFilterList(source.id, false) diff --git a/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt b/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt index ec912bfab..9e88a6571 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/test/TestUtils.kt @@ -13,11 +13,16 @@ import mu.KotlinLogging import org.jetbrains.exposed.dao.id.IdTable import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.deleteAll +import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger +import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.manga.model.table.MangaUserTable fun setLoggingEnabled(enabled: Boolean = true) { val logger = (KotlinLogging.logger(Logger.ROOT_LOGGER_NAME).underlyingLogger as ch.qos.logback.classic.Logger) @@ -34,12 +39,17 @@ fun createLibraryManga( _title: String ): Int { return transaction { - MangaTable.insertAndGetId { + val mangaId = MangaTable.insertAndGetId { it[title] = _title it[url] = _title it[sourceReference] = 1 - it[inLibrary] = true }.value + MangaUserTable.insert { + it[MangaUserTable.manga] = mangaId + it[MangaUserTable.user] = 1 + it[MangaUserTable.inLibrary] = true + } + mangaId } } @@ -66,9 +76,15 @@ fun createChapters( this[ChapterTable.url] = "$it" this[ChapterTable.name] = "$it" this[ChapterTable.sourceOrder] = it - this[ChapterTable.isRead] = read this[ChapterTable.manga] = mangaId } + + val chapters = ChapterTable.select { ChapterTable.manga eq mangaId }.map { it[ChapterTable.id].value } + ChapterMetaTable.batchInsert(chapters) { + this[ChapterUserTable.chapter] = mangaId + this[ChapterUserTable.user] = 1 + this[ChapterUserTable.isRead] = read + } } } From 30b47147162cd2653c6125e5b141ee1d98e94b96 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 29 Jul 2023 20:14:41 -0400 Subject: [PATCH 02/18] Forgot to add new files --- .../tachidesk/global/model/table/UserTable.kt | 17 +++ .../manga/model/table/ChapterUserTable.kt | 31 +++++ .../manga/model/table/MangaUserTable.kt | 28 +++++ .../database/migration/M0030_AddUsers.kt | 115 ++++++++++++++++++ .../tachidesk/server/user/Permissions.kt | 9 ++ .../tachidesk/server/user/UserType.kt | 37 ++++++ 6 files changed, 237 insertions(+) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterUserTable.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaUserTable.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt new file mode 100644 index 000000000..3fbe814c9 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt @@ -0,0 +1,17 @@ +package suwayomi.tachidesk.global.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable + +/** + * Users registered in Tachidesk. + */ +object UserTable : IntIdTable() { + val username = varchar("key", 64) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterUserTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterUserTable.kt new file mode 100644 index 000000000..0cb08828a --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/ChapterUserTable.kt @@ -0,0 +1,31 @@ +package suwayomi.tachidesk.manga.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.leftJoin +import suwayomi.tachidesk.global.model.table.UserTable + +object ChapterUserTable : IntIdTable() { + val chapter = reference("chapter", ChapterTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) + + val isRead = bool("read").default(false) + val isBookmarked = bool("bookmark").default(false) + val lastPageRead = integer("last_page_read").default(0) + val lastReadAt = long("last_read_at").default(0) +} + +fun ChapterTable.getWithUserData(userId: Int) = leftJoin( + ChapterUserTable, + onColumn = { ChapterTable.id }, + otherColumn = { ChapterUserTable.chapter }, + additionalConstraint = { ChapterUserTable.user eq userId } +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaUserTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaUserTable.kt new file mode 100644 index 000000000..52aa2f74e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/MangaUserTable.kt @@ -0,0 +1,28 @@ +package suwayomi.tachidesk.manga.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.leftJoin +import suwayomi.tachidesk.global.model.table.UserTable + +object MangaUserTable : IntIdTable() { + val manga = reference("manga", MangaTable, ReferenceOption.CASCADE) + val user = reference("user", UserTable, ReferenceOption.CASCADE) + val inLibrary = bool("in_library").default(false) + val inLibraryAt = long("in_library_at").default(0) +} + +fun MangaTable.getWithUserData(userId: Int) = leftJoin( + MangaUserTable, + onColumn = { MangaTable.id }, + otherColumn = { MangaUserTable.manga }, + additionalConstraint = { MangaUserTable.user eq userId } +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt new file mode 100644 index 000000000..d74b550d2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt @@ -0,0 +1,115 @@ +package suwayomi.tachidesk.server.database.migration + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import de.neonew.exposed.migrations.helpers.SQLMigration +import org.intellij.lang.annotations.Language + +@Suppress("ClassName", "unused") +class M0030_AddUsers : SQLMigration() { + + @Language("SQL") + override val sql = """ + CREATE TABLE USER + ( + ID INT AUTO_INCREMENT PRIMARY KEY, + USERNAME VARCHAR(64) NOT NULL + ); + + INSERT INTO USER(USERNAME) + SELECT ('admin'); + + -- Step 1: Add USER column to tables CATEGORY, MANGAMETA, CHAPTERMETA, CATEGORYMANGA, GLOBALMETA, and CATEGORYMETA + ALTER TABLE CATEGORY ADD COLUMN USER INT NOT NULL DEFAULT 1; + ALTER TABLE MANGAMETA ADD COLUMN USER INT NOT NULL DEFAULT 1; + ALTER TABLE CHAPTERMETA ADD COLUMN USER INT NOT NULL DEFAULT 1; + ALTER TABLE CATEGORYMANGA ADD COLUMN USER INT NOT NULL DEFAULT 1; + ALTER TABLE GLOBALMETA ADD COLUMN USER INT NOT NULL DEFAULT 1; + ALTER TABLE CATEGORYMETA ADD COLUMN USER INT NOT NULL DEFAULT 1; + + -- Add foreign key constraints to reference USER table + ALTER TABLE CATEGORY ADD CONSTRAINT FK_CATEGORY_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + ALTER TABLE MANGAMETA ADD CONSTRAINT FK_MANGAMETA_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + ALTER TABLE CHAPTERMETA ADD CONSTRAINT FK_CHAPTERMETA_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + ALTER TABLE CATEGORYMANGA ADD CONSTRAINT FK_CATEGORYMANGA_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + ALTER TABLE GLOBALMETA ADD CONSTRAINT FK_GLOBALMETA_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + ALTER TABLE CATEGORYMETA ADD CONSTRAINT FK_CATEGORYMETA_USER FOREIGN KEY (USER) REFERENCES USER(ID) ON DELETE CASCADE; + + ALTER TABLE CATEGORY + ALTER COLUMN USER DROP DEFAULT; + + ALTER TABLE MANGAMETA + ALTER COLUMN USER DROP DEFAULT; + + ALTER TABLE CHAPTERMETA + ALTER COLUMN USER DROP DEFAULT; + + ALTER TABLE CATEGORYMANGA + ALTER COLUMN USER DROP DEFAULT; + + ALTER TABLE GLOBALMETA + ALTER COLUMN USER DROP DEFAULT; + + ALTER TABLE CATEGORYMETA + ALTER COLUMN USER DROP DEFAULT; + + -- Step 2: Create the CHAPTERUSER table + CREATE TABLE CHAPTERUSER + ( + ID INT AUTO_INCREMENT PRIMARY KEY, + LAST_READ_AT BIGINT DEFAULT 0 NOT NULL, + LAST_PAGE_READ INT DEFAULT 0 NOT NULL, + BOOKMARK BOOLEAN DEFAULT FALSE NOT NULL, + READ BOOLEAN DEFAULT FALSE NOT NULL, + CHAPTER INT NOT NULL, + USER INT NOT NULL, + CONSTRAINT FK_CHAPTERUSER_CHAPTER_ID + FOREIGN KEY (CHAPTER) REFERENCES CHAPTER (ID) ON DELETE CASCADE, + CONSTRAINT FK_CHAPTERUSER_USER_ID + FOREIGN KEY (USER) REFERENCES USER (ID) ON DELETE CASCADE + ); + + -- Step 3: Create the MANGAUSER table + CREATE TABLE MANGAUSER + ( + ID INT AUTO_INCREMENT PRIMARY KEY, + IN_LIBRARY BOOLEAN DEFAULT FALSE NOT NULL, + IN_LIBRARY_AT BIGINT DEFAULT 0 NOT NULL, + MANGA INT NOT NULL, + USER INT NOT NULL, + CONSTRAINT FK_MANGAUSER_MANGA_ID + FOREIGN KEY (MANGA) REFERENCES MANGA (ID) ON DELETE CASCADE, + CONSTRAINT FK_MANGAUSER_USER_ID + FOREIGN KEY (USER) REFERENCES USER (ID) ON DELETE CASCADE + ); + + -- Step 4: Backfill the CHAPTERUSER and MANGAUSER tables with existing data + INSERT INTO CHAPTERUSER (LAST_READ_AT, LAST_PAGE_READ, BOOKMARK, READ, CHAPTER, USER) + SELECT LAST_READ_AT, LAST_PAGE_READ, BOOKMARK, READ, ID AS CHAPTER, 1 AS USER + FROM CHAPTER; + + INSERT INTO MANGAUSER (IN_LIBRARY, IN_LIBRARY_AT, MANGA, USER) + SELECT IN_LIBRARY, IN_LIBRARY_AT, ID AS MANGA, 1 AS USER + FROM MANGA; + + -- Step 5: Remove extracted columns from CHAPTER and MANGA tables + ALTER TABLE CHAPTER + DROP COLUMN LAST_READ_AT; + ALTER TABLE CHAPTER + DROP COLUMN LAST_PAGE_READ; + ALTER TABLE CHAPTER + DROP COLUMN BOOKMARK; + ALTER TABLE CHAPTER + DROP COLUMN READ; + + ALTER TABLE MANGA + DROP COLUMN IN_LIBRARY; + ALTER TABLE MANGA + DROP COLUMN IN_LIBRARY_AT; + """.trimIndent() +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt new file mode 100644 index 000000000..d8749e93a --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt @@ -0,0 +1,9 @@ +package suwayomi.tachidesk.server.user + +enum class Permissions { + INSTALL_EXTENSIONS, + INSTALL_UNTRUSTED_EXTENSIONS, + UNINSTALL_EXTENSIONS, + DOWNLOAD_CHAPTERS, + DELETE_DOWNLOADS +} 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 000000000..cc12ea46f --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt @@ -0,0 +1,37 @@ +package suwayomi.tachidesk.server.user + +sealed class UserType { + class Admin(val id: Int) : UserType() + + class User( + val id: Int, + val permissions: List + ) : UserType() + + object Visitor : UserType() +} + +fun UserType.requireUser(): Int { + return when (this) { + is UserType.Admin -> id + is UserType.User -> id + UserType.Visitor -> throw UnauthorizedException() + } +} + +fun UserType.requirePermissions(vararg permissions: Permissions) { + when (this) { + is UserType.Admin -> Unit + is UserType.User -> { + val userPermissions = this.permissions + if (!permissions.all { it in userPermissions }) { + throw ForbiddenException() + } + } + UserType.Visitor -> throw UnauthorizedException() + } +} + +class UnauthorizedException : IllegalStateException("Unauthorized") + +class ForbiddenException : IllegalStateException("Forbidden") From f0b3187198d784601951326269c01310114fc2ec Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 19 Aug 2023 18:59:15 -0400 Subject: [PATCH 03/18] Add user requirements to new gql endpoints --- .../graphql/mutations/DownloadMutation.kt | 49 ++++++++++++++++--- .../graphql/mutations/InfoMutation.kt | 11 ++++- .../graphql/mutations/SettingsMutation.kt | 17 ++++++- .../graphql/mutations/UpdateMutation.kt | 9 +++- 4 files changed, 75 insertions(+), 11 deletions(-) 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 a7741bcce..6c3dcdde2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt @@ -1,16 +1,21 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.model.Status import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -25,7 +30,11 @@ class DownloadMutation { val chapters: List ) - fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DeleteDownloadedChaptersPayload { + fun deleteDownloadedChapters( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteDownloadedChaptersInput + ): DeleteDownloadedChaptersPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input Chapter.deleteChapters(chapters) @@ -48,7 +57,11 @@ class DownloadMutation { val chapters: ChapterType ) - fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DeleteDownloadedChapterPayload { + fun deleteDownloadedChapter( + dataFetchingEnvironment: DataFetchingEnvironment, + input: DeleteDownloadedChapterInput + ): DeleteDownloadedChapterPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input Chapter.deleteChapters(listOf(chapter)) @@ -71,8 +84,10 @@ class DownloadMutation { ) fun enqueueChapterDownloads( + dataFetchingEnvironment: DataFetchingEnvironment, input: EnqueueChapterDownloadsInput ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input DownloadManager.enqueue(DownloadManager.EnqueueInput(chapters)) @@ -97,8 +112,10 @@ class DownloadMutation { ) fun enqueueChapterDownload( + dataFetchingEnvironment: DataFetchingEnvironment, input: EnqueueChapterDownloadInput ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input DownloadManager.enqueue(DownloadManager.EnqueueInput(listOf(chapter))) @@ -123,8 +140,10 @@ class DownloadMutation { ) fun dequeueChapterDownloads( + dataFetchingEnvironment: DataFetchingEnvironment, input: DequeueChapterDownloadsInput ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapters) = input DownloadManager.dequeue(DownloadManager.EnqueueInput(chapters)) @@ -149,8 +168,10 @@ class DownloadMutation { ) fun dequeueChapterDownload( + dataFetchingEnvironment: DataFetchingEnvironment, input: DequeueChapterDownloadInput ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, chapter) = input DownloadManager.dequeue(DownloadManager.EnqueueInput(listOf(chapter))) @@ -173,7 +194,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus ) - fun startDownloader(input: StartDownloaderInput): CompletableFuture { + fun startDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: StartDownloaderInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() DownloadManager.start() return future { @@ -196,7 +221,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus ) - fun stopDownloader(input: StopDownloaderInput): CompletableFuture { + fun stopDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: StopDownloaderInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return future { DownloadManager.stop() StopDownloaderPayload( @@ -218,7 +247,11 @@ class DownloadMutation { val downloadStatus: DownloadStatus ) - fun clearDownloader(input: ClearDownloaderInput): CompletableFuture { + fun clearDownloader( + dataFetchingEnvironment: DataFetchingEnvironment, + input: ClearDownloaderInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return future { DownloadManager.clear() ClearDownloaderPayload( @@ -242,7 +275,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 DownloadManager.reorder(chapter, to) 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 f484cb130..895dc1d4e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -1,13 +1,18 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.WebInterfaceManager import java.util.concurrent.CompletableFuture import kotlin.time.Duration.Companion.seconds @@ -22,7 +27,11 @@ class InfoMutation { val updateStatus: WebUIUpdateStatus ) - fun updateWebUI(input: WebUIUpdateInput): CompletableFuture { + fun updateWebUI( + dataFetchingEnvironment: DataFetchingEnvironment, + input: WebUIUpdateInput + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return future { withTimeout(30.seconds) { if (WebInterfaceManager.status.value.state === DOWNLOADING) { 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 a398c51b0..4873ed9a9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -1,11 +1,16 @@ package suwayomi.tachidesk.graphql.mutations +import graphql.schema.DataFetchingEnvironment +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.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute 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 class SettingsMutation { @@ -60,7 +65,11 @@ class SettingsMutation { if (settings.localSourcePath != null) serverConfig.localSourcePath.value = settings.localSourcePath!! } - fun setSettings(input: SetSettingsInput): SetSettingsPayload { + fun setSettings( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetSettingsInput + ): SetSettingsPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, settings) = input updateSettings(settings) @@ -75,7 +84,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/UpdateMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt index 641c5b3ab..97b773bb8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.kodein.di.DI @@ -50,9 +51,13 @@ class UpdateMutation { val updateStatus: UpdateStatus ) - fun updateCategoryManga(input: UpdateCategoryMangaInput): UpdateCategoryMangaPayload { + fun updateCategoryManga( + dataFetchingEnvironment: DataFetchingEnvironment, + input: UpdateCategoryMangaInput + ): UpdateCategoryMangaPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val categories = transaction { - CategoryTable.select { CategoryTable.id inList input.categories }.map { + CategoryTable.select { CategoryTable.id inList input.categories and (CategoryTable.user eq userId) }.map { CategoryTable.toDataClass(it) } } From fef9ce9a43edb7173cead4e6e8edcb7e7c909e1b Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 19 Aug 2023 19:03:25 -0400 Subject: [PATCH 04/18] Fix actions --- .github/workflows/build_pull_request.yml | 4 ++-- .github/workflows/build_push.yml | 4 ++-- .github/workflows/publish.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 920ef3990..b21f228c0 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -34,10 +34,10 @@ jobs: path: master fetch-depth: 0 - - name: Set up JDK 1.8 + - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 17 - name: Copy CI gradle.properties run: | diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 8b3ba09d7..7a89e73df 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -34,10 +34,10 @@ jobs: path: master fetch-depth: 0 - - name: Set up JDK 1.8 + - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 17 - name: Copy CI gradle.properties run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b221ebeb..73a48f78f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,10 +34,10 @@ jobs: path: master fetch-depth: 0 - - name: Set up JDK 1.8 + - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 1.8 + java-version: 17 - name: Copy CI gradle.properties run: | From 729f91d06e6331b7d698d20962fac4d396786fbc Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 19 Aug 2023 19:14:58 -0400 Subject: [PATCH 05/18] Implement JWT token authentication --- gradle/libs.versions.toml | 4 + server/build.gradle.kts | 3 + .../tachidesk/global/impl/util/Bcrypt.kt | 16 +++ .../tachidesk/global/impl/util/Jwt.kt | 128 ++++++++++++++++++ .../model/table/UserPermissionsTable.kt | 19 +++ .../global/model/table/UserRolesTable.kt | 19 +++ .../tachidesk/global/model/table/UserTable.kt | 3 +- .../graphql/mutations/DownloadMutation.kt | 1 - .../graphql/mutations/InfoMutation.kt | 1 - .../graphql/mutations/SettingsMutation.kt | 1 - .../graphql/mutations/UserMutation.kt | 68 ++++++++++ .../graphql/server/TachideskGraphQLSchema.kt | 4 +- .../suwayomi/tachidesk/server/JavalinSetup.kt | 21 ++- .../suwayomi/tachidesk/server/ServerConfig.kt | 1 + .../database/migration/M0030_AddUsers.kt | 41 +++++- .../tachidesk/server/user/UserType.kt | 2 +- .../src/main/resources/server-reference.conf | 1 + 17 files changed, 316 insertions(+), 17 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Bcrypt.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9c2e92e9..7f55f7571 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -143,6 +143,10 @@ cron4j = "it.sauronsoftware.cron4j:cron4j:2.2.5" # cron-utils cronUtils = "com.cronutils:cron-utils:9.2.0" +# User +bcrypt = "at.favre.lib:bcrypt:0.10.2" +jwt = "com.auth0:java-jwt:4.4.0" + [plugins] # Kotlin kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin"} diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 5bd9ecb04..695aacd76 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -79,6 +79,9 @@ dependencies { implementation(libs.cron4j) implementation(libs.cronUtils) + + implementation(libs.bcrypt) + implementation(libs.jwt) } application { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Bcrypt.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Bcrypt.kt new file mode 100644 index 000000000..794506d0d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Bcrypt.kt @@ -0,0 +1,16 @@ +package suwayomi.tachidesk.global.impl.util + +import at.favre.lib.crypto.bcrypt.BCrypt + +object Bcrypt { + private val hasher = BCrypt.with(BCrypt.Version.VERSION_2B) + private val verifyer = BCrypt.verifyer(BCrypt.Version.VERSION_2B) + + fun encryptPassword(password: String): String { + return hasher.hashToString(12, password.toCharArray()) + } + + fun verify(hash: String, password: String): Boolean { + return verifyer.verify(password.toCharArray(), hash.toCharArray()).verified + } +} 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 000000000..29765797c --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt @@ -0,0 +1,128 @@ +package suwayomi.tachidesk.global.impl.util + +import com.auth0.jwt.JWT +import com.auth0.jwt.JWTVerifier +import com.auth0.jwt.algorithms.Algorithm +import com.auth0.jwt.exceptions.JWTVerificationException +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.global.model.table.UserPermissionsTable +import suwayomi.tachidesk.global.model.table.UserRolesTable +import suwayomi.tachidesk.global.model.table.UserTable +import suwayomi.tachidesk.server.user.Permissions +import suwayomi.tachidesk.server.user.UserType +import java.security.SecureRandom +import java.time.Instant +import javax.crypto.spec.SecretKeySpec +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours + +object Jwt { + private const val ALGORITHM = "HmacSHA256" + private val accessTokenExpiry = 1.hours + private val refreshTokenExpiry = 60.days + private const val ISSUER = "tachidesk" + private const val AUDIENCE = "" // todo audience + + @OptIn(ExperimentalEncodingApi::class) + fun generateSecret(): String { + val keyBytes = ByteArray(32) + SecureRandom().nextBytes(keyBytes) + + val secretKey = SecretKeySpec(keyBytes, ALGORITHM) + + return Base64.encode(secretKey.encoded) + } + + private val algorithm: Algorithm = Algorithm.HMAC256(generateSecret()) // todo store secret + private val verifier: JWTVerifier = JWT.require(algorithm).build() + + class JwtTokens( + val accessToken: String, + val refreshToken: String + ) + + fun generateJwt(userId: Int): JwtTokens { + val accessToken = createAccessToken(userId) + val refreshToken = createRefreshToken(userId) + + return JwtTokens( + accessToken = accessToken, + refreshToken = refreshToken + ) + } + + fun refreshJwt(refreshToken: String): JwtTokens { + val jwt = verifier.verify(refreshToken) + require(jwt.getClaim("token_type").asString() == "refresh") { + "Cannot use access token to refresh" + } + return generateJwt(jwt.subject.toInt()) + } + + fun verifyJwt(jwt: String): UserType { + try { + val decodedJWT = verifier.verify(jwt) + + val user = decodedJWT.subject.toInt() + val roles: List = decodedJWT.getClaim("roles").asList(String::class.java) + val permissions: List = decodedJWT.getClaim("permissions").asList(String::class.java) + + return if (roles.any { it.equals("admin", ignoreCase = true) }) { + UserType.Admin(user) + } else { + UserType.User( + id = user, + permissions = permissions.mapNotNull { permission -> + Permissions.entries.find { it.name == permission } + } + ) + } + } catch (e: JWTVerificationException) { + return UserType.Visitor + } + } + + private fun createAccessToken( + userId: Int + ): String { + val jwt = JWT.create() + .withIssuer(ISSUER) + .withAudience(AUDIENCE) + .withSubject(userId.toString()) + .withClaim("token_type", "access") + .withExpiresAt(Instant.now().plusSeconds(accessTokenExpiry.inWholeSeconds)) + + val user = transaction { + UserTable.select { UserTable.id eq userId }.first() + } + val roles = transaction { + UserRolesTable.select { UserRolesTable.user eq userId }.toList() + .map { it[UserRolesTable.role] } + } + val permissions = transaction { + UserPermissionsTable.select { UserPermissionsTable.user eq userId }.toList() + .map { it[UserPermissionsTable.permission] } + } + + jwt.withClaim("roles", roles) + + jwt.withClaim("permissions", permissions) + + return jwt.sign(algorithm) + } + + private fun createRefreshToken( + userId: Int + ): String { + return JWT.create() + .withIssuer(ISSUER) + .withAudience(AUDIENCE) + .withSubject(userId.toString()) + .withClaim("token_type", "refresh") + .withExpiresAt(Instant.now().plusSeconds(refreshTokenExpiry.inWholeSeconds)) + .sign(algorithm) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt new file mode 100644 index 000000000..05068a572 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt @@ -0,0 +1,19 @@ +package suwayomi.tachidesk.global.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption + +/** + * Users registered in Tachidesk. + */ +object UserPermissionsTable : IntIdTable() { + val user = reference("user", UserTable, ReferenceOption.CASCADE) + val permission = varchar("permission", 128) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt new file mode 100644 index 000000000..5bb1e9c21 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt @@ -0,0 +1,19 @@ +package suwayomi.tachidesk.global.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.ReferenceOption + +/** + * Users registered in Tachidesk. + */ +object UserRolesTable : IntIdTable() { + val user = reference("user", UserTable, ReferenceOption.CASCADE) + val role = varchar("role", 24) +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt index 3fbe814c9..2c86faa5c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserTable.kt @@ -13,5 +13,6 @@ import org.jetbrains.exposed.dao.id.IntIdTable * Users registered in Tachidesk. */ object UserTable : IntIdTable() { - val username = varchar("key", 64) + val username = varchar("username", 64) + val password = varchar("password", 90) } 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 6c3dcdde2..f117b71f3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt @@ -12,7 +12,6 @@ import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.model.Status import suwayomi.tachidesk.manga.model.table.ChapterTable -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.user.requireUser 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 895dc1d4e..96aaff859 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -8,7 +8,6 @@ import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.serverConfig 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 4873ed9a9..ebca91c16 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -5,7 +5,6 @@ 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.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME import suwayomi.tachidesk.server.ServerConfig 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 000000000..08c402a15 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt @@ -0,0 +1,68 @@ +package suwayomi.tachidesk.graphql.mutations + +import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.lowerCase +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.global.impl.util.Bcrypt +import suwayomi.tachidesk.global.impl.util.Jwt +import suwayomi.tachidesk.global.model.table.UserTable +import suwayomi.tachidesk.graphql.server.getAttribute +import suwayomi.tachidesk.server.JavalinSetup +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(JavalinSetup.Attribute.TachideskUser) !is UserType.Visitor) { + throw IllegalArgumentException("Cannot login while already logged-in") + } + val user = transaction { + UserTable.select { UserTable.username.lowerCase() eq input.username.lowercase() }.firstOrNull() + } + if (user != null && Bcrypt.verify(user[UserTable.password], input.password)) { + val jwt = Jwt.generateJwt(user[UserTable.id].value) + 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, + val refreshToken: String + ) + fun refreshToken( + input: RefreshTokenInput + ): RefreshTokenPayload { + val jwt = Jwt.refreshJwt(input.refreshToken) + + return RefreshTokenPayload( + clientMutationId = input.clientMutationId, + accessToken = jwt.accessToken, + refreshToken = jwt.refreshToken + ) + } +} 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 24b552436..fce444944 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -24,6 +24,7 @@ import suwayomi.tachidesk.graphql.mutations.MetaMutation import suwayomi.tachidesk.graphql.mutations.SettingsMutation import suwayomi.tachidesk.graphql.mutations.SourceMutation 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 @@ -84,7 +85,8 @@ val schema = toSchema( TopLevelObject(MetaMutation()), TopLevelObject(SettingsMutation()), TopLevelObject(SourceMutation()), - TopLevelObject(UpdateMutation()) + TopLevelObject(UpdateMutation()), + TopLevelObject(UserMutation()) ), subscriptions = listOf( TopLevelObject(DownloadSubscription()), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index f36f74b36..91d4f704f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -10,6 +10,7 @@ package suwayomi.tachidesk.server import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.path import io.javalin.core.security.RouteRole +import io.javalin.core.util.Header import io.javalin.http.Context import io.javalin.http.HttpCode import io.javalin.http.staticfiles.Location @@ -30,8 +31,10 @@ import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance import suwayomi.tachidesk.global.GlobalAPI +import suwayomi.tachidesk.global.impl.util.Jwt import suwayomi.tachidesk.graphql.GraphQL import suwayomi.tachidesk.manga.MangaAPI +import suwayomi.tachidesk.server.JavalinSetup.setAttribute import suwayomi.tachidesk.server.user.ForbiddenException import suwayomi.tachidesk.server.user.UnauthorizedException import suwayomi.tachidesk.server.user.UserType @@ -95,7 +98,19 @@ object JavalinSetup { return username == serverConfig.basicAuthUsername.value && password == serverConfig.basicAuthPassword.value } - if (serverConfig.basicAuthEnabled.value && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { + val user = if (serverConfig.multiUser.value) { + val authentication = ctx.header(Header.AUTHORIZATION) + if (authentication.isNullOrBlank()) { + UserType.Visitor + } else { + Jwt.verifyJwt(authentication.substringAfter("Bearer ")) + } + } else { + UserType.Admin(1) + } + ctx.setAttribute(Attribute.TachideskUser, user) + + if (!serverConfig.multiUser.value && serverConfig.basicAuthEnabled.value && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { ctx.header("WWW-Authenticate", "Basic") ctx.status(401).json("Unauthorized") } else { @@ -148,10 +163,6 @@ object JavalinSetup { ctx.result(e.message ?: "Forbidden") } - app.before { - it.setAttribute(Attribute.TachideskUser, UserType.Admin(1)) // todo connect to database - } - app.routes { path("api/") { path("v1/") { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index fe94b6f59..516d5d96a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -87,6 +87,7 @@ class ServerConfig(getConfig: () -> Config, val moduleName: String = SERVER_CONF val basicAuthEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) val basicAuthUsername: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) val basicAuthPassword: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val multiUser: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) // misc val debugLogsEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt index d74b550d2..6139bac39 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0030_AddUsers.kt @@ -9,20 +9,44 @@ package suwayomi.tachidesk.server.database.migration import de.neonew.exposed.migrations.helpers.SQLMigration import org.intellij.lang.annotations.Language +import suwayomi.tachidesk.global.impl.util.Bcrypt @Suppress("ClassName", "unused") class M0030_AddUsers : SQLMigration() { - @Language("SQL") - override val sql = """ + class UserSql { + private val password = Bcrypt.encryptPassword("password") + + @Language("SQL") + val sql = """ CREATE TABLE USER ( ID INT AUTO_INCREMENT PRIMARY KEY, - USERNAME VARCHAR(64) NOT NULL + USERNAME VARCHAR(64) NOT NULL, + PASSWORD VARCHAR(90) NOT NULL ); - INSERT INTO USER(USERNAME) - SELECT ('admin'); + INSERT INTO USER(USERNAME, PASSWORD) + SELECT 'admin','$password'; + + CREATE TABLE USERROLES + ( + USER INT NOT NULL, + ROLE VARCHAR(24) NOT NULL, + CONSTRAINT FK_USERROLES_USER_ID + FOREIGN KEY (USER) REFERENCES USER (ID) ON DELETE CASCADE + ); + + INSERT INTO USERROLES(USER, ROLE) + SELECT 1, 'ADMIN'; + + CREATE TABLE USERPERMISSIONS + ( + USER INT NOT NULL, + PERMISSION VARCHAR(128) NOT NULL, + CONSTRAINT FK_USERPERMISSIONS_USER_ID + FOREIGN KEY (USER) REFERENCES USER (ID) ON DELETE CASCADE + ); -- Step 1: Add USER column to tables CATEGORY, MANGAMETA, CHAPTERMETA, CATEGORYMANGA, GLOBALMETA, and CATEGORYMETA ALTER TABLE CATEGORY ADD COLUMN USER INT NOT NULL DEFAULT 1; @@ -111,5 +135,10 @@ class M0030_AddUsers : SQLMigration() { DROP COLUMN IN_LIBRARY; ALTER TABLE MANGA DROP COLUMN IN_LIBRARY_AT; - """.trimIndent() + """.trimIndent() + } + + override val sql by lazy { + UserSql().sql + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt index cc12ea46f..3fa6dfda7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/user/UserType.kt @@ -8,7 +8,7 @@ sealed class UserType { val permissions: List ) : UserType() - object Visitor : UserType() + data object Visitor : UserType() } fun UserType.requireUser(): Int { diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index db38de152..ce2f4b3b9 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -34,6 +34,7 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha server.basicAuthEnabled = false server.basicAuthUsername = "" server.basicAuthPassword = "" +server.multiUser = false # Will ignore basic auth if enabled # misc server.debugLogsEnabled = false From bf43902dce913a78ea1772b774ea0477b1f0f0c7 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 19 Aug 2023 19:20:36 -0400 Subject: [PATCH 06/18] Fix refresh token allowing access --- .../main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 index 29765797c..4b2a93163 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt @@ -66,6 +66,10 @@ object Jwt { try { val decodedJWT = verifier.verify(jwt) + require(decodedJWT.getClaim("token_type").asString() == "access") { + "Cannot use refresh token to access" + } + val user = decodedJWT.subject.toInt() val roles: List = decodedJWT.getClaim("roles").asList(String::class.java) val permissions: List = decodedJWT.getClaim("permissions").asList(String::class.java) From b229edbee2ba378ce2f023c5834d294030f11bff Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 19 Aug 2023 19:44:44 -0400 Subject: [PATCH 07/18] Fixes for database and settings --- .../tachidesk/global/model/table/UserPermissionsTable.kt | 4 ++-- .../suwayomi/tachidesk/global/model/table/UserRolesTable.kt | 4 ++-- .../suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt | 1 + .../kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt | 4 ++++ 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt index 05068a572..1b0eb07b9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserPermissionsTable.kt @@ -7,13 +7,13 @@ package suwayomi.tachidesk.global.model.table * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table /** * Users registered in Tachidesk. */ -object UserPermissionsTable : IntIdTable() { +object UserPermissionsTable : Table() { val user = reference("user", UserTable, ReferenceOption.CASCADE) val permission = varchar("permission", 128) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt index 5bb1e9c21..551b386f6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/model/table/UserRolesTable.kt @@ -7,13 +7,13 @@ package suwayomi.tachidesk.global.model.table * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.Table /** * Users registered in Tachidesk. */ -object UserRolesTable : IntIdTable() { +object UserRolesTable : Table() { val user = reference("user", UserTable, ReferenceOption.CASCADE) val role = varchar("role", 24) } 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 ebca91c16..b320f10f7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SettingsMutation.kt @@ -52,6 +52,7 @@ class SettingsMutation { if (settings.basicAuthEnabled != null) serverConfig.basicAuthEnabled.value = settings.basicAuthEnabled!! if (settings.basicAuthUsername != null) serverConfig.basicAuthUsername.value = settings.basicAuthUsername!! if (settings.basicAuthPassword != null) serverConfig.basicAuthPassword.value = settings.basicAuthPassword!! + if (settings.multiUser != null) serverConfig.multiUser.value = settings.multiUser!! if (settings.debugLogsEnabled != null) serverConfig.debugLogsEnabled.value = settings.debugLogsEnabled!! if (settings.systemTrayEnabled != null) serverConfig.systemTrayEnabled.value = settings.systemTrayEnabled!! diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt index 22a426ab5..e43c40eac 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt @@ -51,6 +51,7 @@ interface Settings : Node { val basicAuthEnabled: Boolean? val basicAuthUsername: String? val basicAuthPassword: String? + val multiUser: Boolean? // misc val debugLogsEnabled: Boolean? @@ -101,6 +102,7 @@ data class PartialSettingsType( override val basicAuthEnabled: Boolean?, override val basicAuthUsername: String?, override val basicAuthPassword: String?, + override val multiUser: Boolean?, // misc override val debugLogsEnabled: Boolean?, @@ -151,6 +153,7 @@ class SettingsType( override val basicAuthEnabled: Boolean, override val basicAuthUsername: String, override val basicAuthPassword: String, + override val multiUser: Boolean?, // misc override val debugLogsEnabled: Boolean, @@ -194,6 +197,7 @@ class SettingsType( config.basicAuthEnabled.value, config.basicAuthUsername.value, config.basicAuthPassword.value, + config.multiUser.value, config.debugLogsEnabled.value, config.systemTrayEnabled.value, From 684562c4f78a6ba0fbb9aac6d31267edaf971413 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 20 Aug 2023 11:38:57 -0400 Subject: [PATCH 08/18] Add user requirements to more endpoints --- .../tachidesk/global/impl/util/Jwt.kt | 3 -- .../tachidesk/graphql/queries/InfoQuery.kt | 28 ++++++++++++++++--- .../graphql/queries/SettingsQuery.kt | 9 +++++- .../tachidesk/graphql/queries/UpdateQuery.kt | 15 ++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) 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 index 4b2a93163..5eef56751 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt @@ -99,9 +99,6 @@ object Jwt { .withClaim("token_type", "access") .withExpiresAt(Instant.now().plusSeconds(accessTokenExpiry.inWholeSeconds)) - val user = transaction { - UserTable.select { UserTable.id eq userId }.first() - } val roles = transaction { UserRolesTable.select { UserRolesTable.user eq userId }.toList() .map { it[UserRolesTable.role] } 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 45ed95fc3..990f16c39 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -1,11 +1,16 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment import suwayomi.tachidesk.global.impl.AppUpdate +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.server.BuildConfig +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.serverConfig +import suwayomi.tachidesk.server.user.requireUser import suwayomi.tachidesk.server.util.WebInterfaceManager import java.util.concurrent.CompletableFuture @@ -20,7 +25,11 @@ class InfoQuery { val discord: String ) - fun about(): AboutPayload { + fun about( + dataFetchingEnvironment: DataFetchingEnvironment, + ): AboutPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return AboutPayload( BuildConfig.NAME, BuildConfig.VERSION, @@ -39,7 +48,11 @@ class InfoQuery { val url: String ) - fun checkForServerUpdates(): CompletableFuture> { + fun checkForServerUpdates( + dataFetchingEnvironment: DataFetchingEnvironment, + ): CompletableFuture> { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + return future { AppUpdate.checkUpdate().map { CheckForServerUpdatesPayload( @@ -51,7 +64,10 @@ class InfoQuery { } } - fun checkForWebUIUpdate(): CompletableFuture { + fun checkForWebUIUpdate( + dataFetchingEnvironment: DataFetchingEnvironment + ): CompletableFuture { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return future { val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() WebUIUpdateInfo( @@ -62,7 +78,11 @@ class InfoQuery { } } - fun getWebUIUpdateStatus(): WebUIUpdateStatus { + 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/SettingsQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt index 6be6aafd8..03fee28a4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt @@ -1,9 +1,16 @@ 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.user.requireUser class SettingsQuery { - fun settings(): SettingsType { + fun settings( + dataFetchingEnvironment: DataFetchingEnvironment, + ): SettingsType { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return SettingsType() } } 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 344c054bb..1781b7709 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt @@ -1,21 +1,32 @@ package suwayomi.tachidesk.graphql.queries +import graphql.schema.DataFetchingEnvironment import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.requireUser class UpdateQuery { private val updater by DI.global.instance() - fun updateStatus(): UpdateStatus { + fun updateStatus( + dataFetchingEnvironment: DataFetchingEnvironment, + ): UpdateStatus { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return UpdateStatus(updater.status.value) } data class LastUpdateTimestampPayload(val timestamp: Long) - fun lastUpdateTimestamp(): LastUpdateTimestampPayload { + fun lastUpdateTimestamp( + dataFetchingEnvironment: DataFetchingEnvironment, + ): LastUpdateTimestampPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return LastUpdateTimestampPayload(updater.getLastUpdateTimestamp()) } } From 283e537e83c14e94cbe6bca81d5f42a6c7adfeaf Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 20 Aug 2023 12:26:21 -0400 Subject: [PATCH 09/18] Implement registration --- .../graphql/mutations/UserMutation.kt | 39 ++++++++++++++++++- .../tachidesk/server/user/Permissions.kt | 3 +- .../src/main/resources/server-reference.conf | 4 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt index 08c402a15..6bb42dd58 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt @@ -1,6 +1,7 @@ package suwayomi.tachidesk.graphql.mutations import graphql.schema.DataFetchingEnvironment +import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction @@ -8,8 +9,12 @@ import suwayomi.tachidesk.global.impl.util.Bcrypt import suwayomi.tachidesk.global.impl.util.Jwt import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.graphql.server.getAttribute +import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute +import suwayomi.tachidesk.server.user.Permissions import suwayomi.tachidesk.server.user.UserType +import suwayomi.tachidesk.server.user.requirePermissions class UserMutation { @@ -27,7 +32,7 @@ class UserMutation { dataFetchingEnvironment: DataFetchingEnvironment, input: LoginInput ): LoginPayload { - if (dataFetchingEnvironment.getAttribute(JavalinSetup.Attribute.TachideskUser) !is UserType.Visitor) { + if (dataFetchingEnvironment.getAttribute(Attribute.TachideskUser) !is UserType.Visitor) { throw IllegalArgumentException("Cannot login while already logged-in") } val user = transaction { @@ -65,4 +70,36 @@ class UserMutation { refreshToken = jwt.refreshToken ) } + + data class RegisterInput( + val clientMutationId: String? = null, + val username: String, + val password: String, + ) + data class RegisterPayload( + val clientMutationId: String?, + ) + fun register( + dataFetchingEnvironment: DataFetchingEnvironment, + input: RegisterInput + ): RegisterPayload { + dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requirePermissions(Permissions.CREATE_USER) + + val (clientMutationId, username, password) = input + transaction { + val userExists = UserTable.select { UserTable.username.lowerCase() eq username.lowercase() }.isNotEmpty() + if (userExists) { + throw Exception("Username already exists") + } else { + UserTable.insert { + it[UserTable.username] = username + it[UserTable.password] = Bcrypt.encryptPassword(password) + } + } + } + + return RegisterPayload( + clientMutationId = clientMutationId, + ) + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt index d8749e93a..d17100fa2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/user/Permissions.kt @@ -5,5 +5,6 @@ enum class Permissions { INSTALL_UNTRUSTED_EXTENSIONS, UNINSTALL_EXTENSIONS, DOWNLOAD_CHAPTERS, - DELETE_DOWNLOADS + DELETE_DOWNLOADS, + CREATE_USER } diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index ce2f4b3b9..a01eb8049 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -30,10 +30,12 @@ server.excludeNotStarted = true server.excludeCompleted = true server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't have to be full hours e.g. 12.5) - range: 6 <= n < ∞ - default: 12 hours - interval in which the global update will be automatically triggered -# Authentication +# authentication server.basicAuthEnabled = false server.basicAuthUsername = "" server.basicAuthPassword = "" + +# user server.multiUser = false # Will ignore basic auth if enabled # misc From 1ba3d70fe0e95535f78816f7e5f4f08146051d0e Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 20 Aug 2023 12:30:16 -0400 Subject: [PATCH 10/18] Implement change password endpoint --- .../graphql/mutations/UserMutation.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt index 6bb42dd58..8ade994d3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt @@ -5,6 +5,7 @@ import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.global.impl.util.Bcrypt import suwayomi.tachidesk.global.impl.util.Jwt import suwayomi.tachidesk.global.model.table.UserTable @@ -15,6 +16,7 @@ import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.user.Permissions import suwayomi.tachidesk.server.user.UserType import suwayomi.tachidesk.server.user.requirePermissions +import suwayomi.tachidesk.server.user.requireUser class UserMutation { @@ -102,4 +104,29 @@ class UserMutation { clientMutationId = clientMutationId, ) } + + data class SetPasswordInput( + val clientMutationId: String? = null, + val password: String, + ) + data class SetPasswordPayload( + val clientMutationId: String?, + ) + fun setPassword( + dataFetchingEnvironment: DataFetchingEnvironment, + input: SetPasswordInput + ): SetPasswordPayload { + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + + val (clientMutationId, password) = input + transaction { + UserTable.update({ UserTable.id eq userId }) { + it[UserTable.password] = Bcrypt.encryptPassword(password) + } + } + + return SetPasswordPayload( + clientMutationId = clientMutationId, + ) + } } From b5b61e4c5135e4adb1a259789c6c1df32dbbf0f8 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 27 Aug 2023 21:58:01 -0400 Subject: [PATCH 11/18] Cleanup --- .../suwayomi/tachidesk/global/impl/util/Jwt.kt | 1 - .../tachidesk/graphql/mutations/UserMutation.kt | 13 ++++++------- .../suwayomi/tachidesk/graphql/queries/InfoQuery.kt | 5 ++--- .../tachidesk/graphql/queries/SettingsQuery.kt | 2 +- .../tachidesk/graphql/queries/UpdateQuery.kt | 5 ++--- 5 files changed, 11 insertions(+), 15 deletions(-) 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 index 5eef56751..a2dfae4af 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt @@ -8,7 +8,6 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.model.table.UserPermissionsTable import suwayomi.tachidesk.global.model.table.UserRolesTable -import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.server.user.Permissions import suwayomi.tachidesk.server.user.UserType import java.security.SecureRandom diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt index 8ade994d3..af60651df 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UserMutation.kt @@ -11,7 +11,6 @@ import suwayomi.tachidesk.global.impl.util.Jwt import suwayomi.tachidesk.global.model.table.UserTable import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.user.Permissions import suwayomi.tachidesk.server.user.UserType @@ -76,10 +75,10 @@ class UserMutation { data class RegisterInput( val clientMutationId: String? = null, val username: String, - val password: String, + val password: String ) data class RegisterPayload( - val clientMutationId: String?, + val clientMutationId: String? ) fun register( dataFetchingEnvironment: DataFetchingEnvironment, @@ -101,16 +100,16 @@ class UserMutation { } return RegisterPayload( - clientMutationId = clientMutationId, + clientMutationId = clientMutationId ) } data class SetPasswordInput( val clientMutationId: String? = null, - val password: String, + val password: String ) data class SetPasswordPayload( - val clientMutationId: String?, + val clientMutationId: String? ) fun setPassword( dataFetchingEnvironment: DataFetchingEnvironment, @@ -126,7 +125,7 @@ class UserMutation { } return SetPasswordPayload( - clientMutationId = clientMutationId, + clientMutationId = clientMutationId ) } } 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 990f16c39..6de68efac 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -6,7 +6,6 @@ import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.server.BuildConfig -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.serverConfig @@ -26,7 +25,7 @@ class InfoQuery { ) fun about( - dataFetchingEnvironment: DataFetchingEnvironment, + dataFetchingEnvironment: DataFetchingEnvironment ): AboutPayload { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() @@ -49,7 +48,7 @@ class InfoQuery { ) fun checkForServerUpdates( - dataFetchingEnvironment: DataFetchingEnvironment, + dataFetchingEnvironment: DataFetchingEnvironment ): CompletableFuture> { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() 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 03fee28a4..5001f6b3f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/SettingsQuery.kt @@ -8,7 +8,7 @@ import suwayomi.tachidesk.server.user.requireUser class SettingsQuery { fun settings( - dataFetchingEnvironment: DataFetchingEnvironment, + dataFetchingEnvironment: DataFetchingEnvironment ): SettingsType { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return SettingsType() 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 1781b7709..8645a6152 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt @@ -7,7 +7,6 @@ import org.kodein.di.instance import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.manga.impl.update.IUpdater -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.user.requireUser @@ -15,7 +14,7 @@ class UpdateQuery { private val updater by DI.global.instance() fun updateStatus( - dataFetchingEnvironment: DataFetchingEnvironment, + dataFetchingEnvironment: DataFetchingEnvironment ): UpdateStatus { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return UpdateStatus(updater.status.value) @@ -24,7 +23,7 @@ class UpdateQuery { data class LastUpdateTimestampPayload(val timestamp: Long) fun lastUpdateTimestamp( - dataFetchingEnvironment: DataFetchingEnvironment, + dataFetchingEnvironment: DataFetchingEnvironment ): LastUpdateTimestampPayload { dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() return LastUpdateTimestampPayload(updater.getLastUpdateTimestamp()) From 82a9003473e82dcd96e2019807d7369066ce2937 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sun, 27 Aug 2023 21:58:14 -0400 Subject: [PATCH 12/18] Probably fix updater --- .../kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index f19fce939..b33c31a57 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -26,10 +26,8 @@ import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update -import suwayomi.tachidesk.manga.impl.Manga.getManga import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput -import suwayomi.tachidesk.manga.impl.util.lang.isEmpty import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass @@ -115,12 +113,12 @@ object Chapter { } suspend fun fetchChapterList(userId: Int, mangaId: Int): List { - val manga = getManga(userId, mangaId) - val source = getCatalogueSourceOrStub(manga.sourceId.toLong()) + val manga = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() } + val source = getCatalogueSourceOrStub(manga[MangaTable.sourceReference]) val sManga = SManga.create().apply { - title = manga.title - url = manga.url + title = manga[MangaTable.title] + url = manga[MangaTable.url] } val numberOfCurrentChapters = getCountOfMangaChapters(mangaId) @@ -129,7 +127,7 @@ object Chapter { // Recognize number for new chapters. chapterList.forEach { chapter -> (source as? HttpSource)?.prepareNewChapter(chapter, sManga) - val chapterNumber = ChapterRecognition.parseChapterNumber(manga.title, chapter.name, chapter.chapter_number.toDouble()) + val chapterNumber = ChapterRecognition.parseChapterNumber(manga[MangaTable.title], chapter.name, chapter.chapter_number.toDouble()) chapter.chapter_number = chapterNumber.toFloat() } From 0c5e86a1302d12ed431bcd08064e14351904cbd1 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 1 Sep 2023 18:54:22 -0400 Subject: [PATCH 13/18] Revert to Java 8 and use another way to get user id in data loaders --- .github/workflows/build_pull_request.yml | 2 +- .github/workflows/build_push.yml | 2 +- .github/workflows/publish.yml | 2 +- build.gradle.kts | 6 ++--- gradle/libs.versions.toml | 2 +- .../graphql/dataLoaders/CategoryDataLoader.kt | 13 +++++----- .../graphql/dataLoaders/ChapterDataLoader.kt | 9 +++---- .../dataLoaders/ExtensionDataLoader.kt | 5 ++-- .../graphql/dataLoaders/MangaDataLoader.kt | 17 ++++++------ .../graphql/dataLoaders/MetaDataLoader.kt | 26 ++++++++++++------- .../graphql/dataLoaders/SourceDataLoader.kt | 5 ++-- .../server/TachideskGraphQLContextFactory.kt | 23 +++++++++++++--- .../ApolloSubscriptionProtocolHandler.kt | 2 +- .../ApolloSubscriptionSessionState.kt | 2 +- .../GraphQLSubscriptionHandler.kt | 4 +-- 15 files changed, 69 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index b21f228c0..fdc0b3d10 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -37,7 +37,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 17 + java-version: 1.8 - name: Copy CI gradle.properties run: | diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 7a89e73df..b98d42fa9 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -37,7 +37,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 17 + java-version: 1.8 - name: Copy CI gradle.properties run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 73a48f78f..9b8765295 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -37,7 +37,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v1 with: - java-version: 17 + java-version: 1.8 - name: Copy CI gradle.properties run: | diff --git a/build.gradle.kts b/build.gradle.kts index d27aa5e57..93a920ea4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -27,8 +27,8 @@ allprojects { subprojects { plugins.withType { extensions.configure { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } } @@ -36,7 +36,7 @@ subprojects { withType { dependsOn("formatKotlin") kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() + jvmTarget = JavaVersion.VERSION_1_8.toString() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7f55f7571..e8a5013f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ rhino = "1.7.14" settings = "1.0.0-RC" twelvemonkeys = "3.9.4" playwright = "1.28.0" -graphqlkotlin = "7.0.0-alpha.6" +graphqlkotlin = "6.5.3" xmlserialization = "0.86.1" [libraries] diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt index 6fa4713b1..0811ec711 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/CategoryDataLoader.kt @@ -8,7 +8,6 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -28,9 +27,9 @@ import suwayomi.tachidesk.server.user.requireUser class CategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoryDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val categories = CategoryTable.select { CategoryTable.id inList ids and (CategoryTable.user eq userId) } @@ -44,9 +43,9 @@ class CategoryDataLoader : KotlinDataLoader { class CategoryForIdsDataLoader : KotlinDataLoader, CategoryNodeList> { override val dataLoaderName = "CategoryForIdsDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader, CategoryNodeList> = DataLoaderFactory.newDataLoader { categoryIds -> + override fun getDataLoader(): DataLoader, CategoryNodeList> = DataLoaderFactory.newDataLoader { categoryIds, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val ids = categoryIds.flatten().distinct() @@ -61,9 +60,9 @@ class CategoryForIdsDataLoader : KotlinDataLoader, CategoryNodeList> { class CategoriesForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "CategoriesForMangaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val itemsByRef = CategoryMangaTable.innerJoin(CategoryTable) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 2c51773eb..e3c483836 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -8,7 +8,6 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -27,9 +26,9 @@ import suwayomi.tachidesk.server.user.requireUser class ChapterDataLoader : KotlinDataLoader { override val dataLoaderName = "ChapterDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val chapters = ChapterTable.getWithUserData(userId) @@ -44,9 +43,9 @@ class ChapterDataLoader : KotlinDataLoader { class ChaptersForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "ChaptersForMangaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val chaptersByMangaId = ChapterTable.getWithUserData(userId) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt index 619220184..7e8c0763c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ExtensionDataLoader.kt @@ -8,7 +8,6 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -22,7 +21,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future class ExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -37,7 +36,7 @@ class ExtensionDataLoader : KotlinDataLoader { class ExtensionForSourceDataLoader : KotlinDataLoader { override val dataLoaderName = "ExtensionForSourceDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 1be2c780c..757bf0807 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -8,7 +8,6 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -31,9 +30,9 @@ import suwayomi.tachidesk.server.user.requireUser class MangaDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val manga = MangaTable.getWithUserData(userId) @@ -48,9 +47,9 @@ class MangaDataLoader : KotlinDataLoader { class MangaForCategoryDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaForCategoryDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val itemsByRef = if (ids.contains(0)) { @@ -78,9 +77,9 @@ class MangaForCategoryDataLoader : KotlinDataLoader { class MangaForSourceDataLoader : KotlinDataLoader { override val dataLoaderName = "MangaForSourceDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val mangaBySourceId = MangaTable.getWithUserData(userId) @@ -95,9 +94,9 @@ class MangaForSourceDataLoader : KotlinDataLoader { class MangaForIdsDataLoader : KotlinDataLoader, MangaNodeList> { override val dataLoaderName = "MangaForIdsDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader, MangaNodeList> = DataLoaderFactory.newDataLoader { mangaIds -> + override fun getDataLoader(): DataLoader, MangaNodeList> = DataLoaderFactory.newDataLoader { mangaIds, env -> future { - val userId = graphQLContext.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val ids = mangaIds.flatten().distinct() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index d58e35a8a..89345574f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -1,14 +1,15 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger import org.jetbrains.exposed.sql.addLogger +import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.global.model.table.GlobalMetaTable +import suwayomi.tachidesk.graphql.server.getAttribute import suwayomi.tachidesk.graphql.types.CategoryMetaType import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType @@ -16,15 +17,19 @@ import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.user.requireUser class GlobalMetaDataLoader : KotlinDataLoader { override val dataLoaderName = "GlobalMetaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids, env -> future { + val userId = env.getAttribute(Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids } + val metasByRefId = GlobalMetaTable.select { GlobalMetaTable.key inList ids and (GlobalMetaTable.user eq userId) } .map { GlobalMetaType(it) } .associateBy { it.key } ids.map { metasByRefId[it] } @@ -35,11 +40,12 @@ class GlobalMetaDataLoader : KotlinDataLoader { class ChapterMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "ChapterMetaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids, env -> future { + val userId = env.getAttribute(Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids } + val metasByRefId = ChapterMetaTable.select { ChapterMetaTable.ref inList ids and (ChapterMetaTable.user eq userId) } .map { ChapterMetaType(it) } .groupBy { it.chapterId } ids.map { metasByRefId[it].orEmpty() } @@ -50,11 +56,12 @@ class ChapterMetaDataLoader : KotlinDataLoader> { class MangaMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "MangaMetaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids, env -> future { + val userId = env.getAttribute(Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids } + val metasByRefId = MangaMetaTable.select { MangaMetaTable.ref inList ids and (MangaMetaTable.user eq userId) } .map { MangaMetaType(it) } .groupBy { it.mangaId } ids.map { metasByRefId[it].orEmpty() } @@ -65,11 +72,12 @@ class MangaMetaDataLoader : KotlinDataLoader> { class CategoryMetaDataLoader : KotlinDataLoader> { override val dataLoaderName = "CategoryMetaDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader> = DataLoaderFactory.newDataLoader> { ids -> + override fun getDataLoader(): DataLoader> = DataLoaderFactory.newDataLoader> { ids, env -> future { + val userId = env.getAttribute(Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) - val metasByRefId = CategoryMetaTable.select { CategoryMetaTable.ref inList ids } + val metasByRefId = CategoryMetaTable.select { CategoryMetaTable.ref inList ids and (CategoryMetaTable.user eq userId) } .map { CategoryMetaType(it) } .groupBy { it.categoryId } ids.map { metasByRefId[it].orEmpty() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt index 611ad1116..bc58ac6f6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/SourceDataLoader.kt @@ -8,7 +8,6 @@ package suwayomi.tachidesk.graphql.dataLoaders import com.expediagroup.graphql.dataloader.KotlinDataLoader -import graphql.GraphQLContext import org.dataloader.DataLoader import org.dataloader.DataLoaderFactory import org.jetbrains.exposed.sql.Slf4jSqlDebugLogger @@ -24,7 +23,7 @@ import suwayomi.tachidesk.server.JavalinSetup.future class SourceDataLoader : KotlinDataLoader { override val dataLoaderName = "SourceDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) @@ -39,7 +38,7 @@ class SourceDataLoader : KotlinDataLoader { class SourcesForExtensionDataLoader : KotlinDataLoader { override val dataLoaderName = "SourcesForExtensionDataLoader" - override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader = DataLoaderFactory.newDataLoader { ids -> + override fun getDataLoader(): DataLoader = DataLoaderFactory.newDataLoader { ids -> future { transaction { addLogger(Slf4jSqlDebugLogger) 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 12b0895f3..675028698 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt @@ -7,24 +7,25 @@ package suwayomi.tachidesk.graphql.server -import com.expediagroup.graphql.generator.extensions.toGraphQLContext 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 /** * Custom logic for how Tachidesk should create its context given the [Context] */ -class TachideskGraphQLContextFactory : GraphQLContextFactory { - override suspend fun generateContext(request: Context): GraphQLContext { +@Suppress("DEPRECATION") +class TachideskGraphQLContextFactory : GraphQLContextFactory { + override suspend fun generateContextMap(request: Context): Map { return mapOf( Context::class to request, request.getPair(Attribute.TachideskUser) - ).toGraphQLContext() + ) } private fun Context.getPair(attribute: Attribute) = @@ -33,6 +34,13 @@ class TachideskGraphQLContextFactory : GraphQLContextFactory { fun generateContextMap(request: WsContext): Map<*, Any> = emptyMap() } +/** + * Create a [GraphQLContext] from [this] map + * @return a new [GraphQLContext] + */ +fun Map<*, Any?>.toGraphQLContext(): GraphQLContext = + GraphQLContext.of(this) + fun GraphQLContext.getAttribute(attribute: Attribute): T { return get(attribute) } @@ -40,3 +48,10 @@ fun GraphQLContext.getAttribute(attribute: Attribute): T { fun DataFetchingEnvironment.getAttribute(attribute: Attribute): T { return graphQlContext.get(attribute) } + +val BatchLoaderEnvironment.graphQlContext: GraphQLContext + get() = keyContextsList.filterIsInstance().first() + +fun BatchLoaderEnvironment.getAttribute(attribute: Attribute): T { + return graphQlContext.getAttribute(attribute) +} 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 531da3c5c..fcccac791 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 @@ -7,7 +7,6 @@ package suwayomi.tachidesk.graphql.server.subscriptions -import com.expediagroup.graphql.generator.extensions.toGraphQLContext import com.expediagroup.graphql.server.types.GraphQLRequest import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.convertValue @@ -35,6 +34,7 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_CONNECTION_ACK 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 /** * Implementation of the `graphql-ws` protocol defined by Apollo diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt index f7fe44792..7576ee2c4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt @@ -7,7 +7,6 @@ package suwayomi.tachidesk.graphql.server.subscriptions -import com.expediagroup.graphql.generator.extensions.toGraphQLContext import graphql.GraphQLContext import io.javalin.websocket.WsContext import kotlinx.coroutines.Job @@ -15,6 +14,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.onCompletion import org.eclipse.jetty.websocket.api.CloseStatus +import suwayomi.tachidesk.graphql.server.toGraphQLContext import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt index 5e92768c8..a402e6421 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/GraphQLSubscriptionHandler.kt @@ -29,8 +29,8 @@ open class GraphQLSubscriptionHandler( graphQLRequest: GraphQLRequest, graphQLContext: GraphQLContext = GraphQLContext.of(emptyMap()) ): Flow> { - val dataLoaderRegistry = dataLoaderRegistryFactory?.generate(graphQLContext) - val input = graphQLRequest.toExecutionInput(graphQLContext, dataLoaderRegistry) + val dataLoaderRegistry = dataLoaderRegistryFactory?.generate() + val input = graphQLRequest.toExecutionInput(dataLoaderRegistry, graphQLContext) val res = graphQL.execute(input) val data = res.getData>() From 3792513b4ad5890d8b232ae213b5c07fbc6083a1 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 1 Sep 2023 18:54:42 -0400 Subject: [PATCH 14/18] Authenticate websockets --- .../server/TachideskGraphQLContextFactory.kt | 11 +++++-- .../suwayomi/tachidesk/server/JavalinSetup.kt | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) 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 675028698..c05e3b6db 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLContextFactory.kt @@ -28,10 +28,17 @@ class TachideskGraphQLContextFactory : GraphQLContextFactory { + return mapOf( + Context::class to request, + request.getPair(Attribute.TachideskUser) + ) + } + private fun Context.getPair(attribute: Attribute) = attribute to getAttribute(attribute) - - fun generateContextMap(request: WsContext): Map<*, Any> = emptyMap() + private fun WsContext.getPair(attribute: Attribute) = + attribute to getAttribute(attribute) } /** diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 91d4f704f..a5250577d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -17,6 +17,8 @@ import io.javalin.http.staticfiles.Location import io.javalin.plugin.openapi.OpenApiOptions import io.javalin.plugin.openapi.OpenApiPlugin import io.javalin.plugin.openapi.ui.SwaggerOptions +import io.javalin.websocket.WsConnectContext +import io.javalin.websocket.WsContext import io.swagger.v3.oas.models.info.Info import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -125,6 +127,27 @@ object JavalinSetup { } }.start() + app.wsBefore { + it.onConnect { ctx -> + val user = if (serverConfig.multiUser.value) { + val authentication = ctx.header(Header.AUTHORIZATION) + if (authentication.isNullOrBlank()) { + val token = ctx.queryParam("token") + if (token.isNullOrBlank()) { + UserType.Visitor + } else { + Jwt.verifyJwt(token) + } + } else { + Jwt.verifyJwt(authentication.substringAfter("Bearer ")) + } + } else { + UserType.Admin(1) + } + ctx.setAttribute(Attribute.TachideskUser, user) + } + } + // when JVM is prompted to shutdown, stop javalin gracefully Runtime.getRuntime().addShutdownHook( thread(start = false) { @@ -201,7 +224,15 @@ object JavalinSetup { attribute(attribute.name, value) } + private fun WsContext.setAttribute(attribute: Attribute, value: T) { + attribute(attribute.name, value) + } + fun Context.getAttribute(attribute: Attribute): T { return attribute(attribute.name)!! } + + fun WsContext.getAttribute(attribute: Attribute): T { + return attribute(attribute.name)!! + } } From 78c8321f642ac61d530d95d8b78f9dfa54fc1179 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 1 Sep 2023 18:54:56 -0400 Subject: [PATCH 15/18] Handle forbidden exceptions --- .../server/TachideskDataFetcherExceptionHandler.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt index bc4a7af57..cb17be6f7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataFetcherExceptionHandler.kt @@ -7,11 +7,12 @@ import graphql.execution.DataFetcherExceptionHandlerResult import graphql.execution.SimpleDataFetcherExceptionHandler import io.javalin.http.Context import io.javalin.http.HttpCode +import suwayomi.tachidesk.server.user.ForbiddenException import suwayomi.tachidesk.server.user.UnauthorizedException class TachideskDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() { - @Suppress("OVERRIDE_DEPRECATION", "DEPRECATION") + @Suppress("OVERRIDE_DEPRECATION") override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult { val exception = handlerParameters.exception if (exception is UnauthorizedException) { @@ -21,6 +22,13 @@ class TachideskDataFetcherExceptionHandler : SimpleDataFetcherExceptionHandler() handlerParameters.dataFetchingEnvironment.getFromContext()?.status(HttpCode.UNAUTHORIZED) return DataFetcherExceptionHandlerResult.newResult().error(error).build() } + if (exception is ForbiddenException) { + val error = ExceptionWhileDataFetching(handlerParameters.path, exception, handlerParameters.sourceLocation) + logException(error, exception) + // Set the HTTP status code to 403 + handlerParameters.dataFetchingEnvironment.getFromContext()?.status(HttpCode.FORBIDDEN) + return DataFetcherExceptionHandlerResult.newResult().error(error).build() + } return super.onException(handlerParameters) } } From 39e4da22e9a094baa8e9ed14c4b6678c50d37882 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Fri, 1 Sep 2023 22:01:30 -0400 Subject: [PATCH 16/18] Minor cleanup --- .../suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt | 1 - server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index 89345574f..28a2be11a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -17,7 +17,6 @@ import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable -import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.Attribute import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.user.requireUser diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index a5250577d..011486a06 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -17,7 +17,6 @@ import io.javalin.http.staticfiles.Location import io.javalin.plugin.openapi.OpenApiOptions import io.javalin.plugin.openapi.OpenApiPlugin import io.javalin.plugin.openapi.ui.SwaggerOptions -import io.javalin.websocket.WsConnectContext import io.javalin.websocket.WsContext import io.swagger.v3.oas.models.info.Info import kotlinx.coroutines.CoroutineScope From 85eba8247236a5e8468c31dd7b4c8d5854379b1a Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 7 Oct 2023 21:12:31 -0400 Subject: [PATCH 17/18] Data object --- .../src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index a84351666..52267e036 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -225,7 +225,7 @@ object JavalinSetup { } sealed class Attribute(val name: String) { - object TachideskUser : Attribute("user") + data object TachideskUser : Attribute("user") } private fun Context.setAttribute( From 37cd93b86468a5a9829b23ce2ea833a33ae1d7d1 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Sat, 7 Oct 2023 21:40:54 -0400 Subject: [PATCH 18/18] Fix build --- .../graphql/dataLoaders/ChapterDataLoader.kt | 11 +++++++---- .../tachidesk/graphql/mutations/DownloadMutation.kt | 4 ++-- .../kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt | 10 ++++++++-- .../kotlin/suwayomi/tachidesk/manga/impl/Manga.kt | 13 ++++++++++--- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt index 860b51445..4daa91207 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/ChapterDataLoader.kt @@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.types.ChapterNodeList import suwayomi.tachidesk.graphql.types.ChapterNodeList.Companion.toNodeList import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.ChapterUserTable import suwayomi.tachidesk.manga.model.table.getWithUserData import suwayomi.tachidesk.server.JavalinSetup import suwayomi.tachidesk.server.JavalinSetup.future @@ -91,16 +92,17 @@ class UnreadChapterCountForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "UnreadChapterCountForMangaDataLoader" override fun getDataLoader(): DataLoader = - DataLoaderFactory.newDataLoader { ids -> + DataLoaderFactory.newDataLoader { ids, env -> future { + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val unreadChapterCountByMangaId = ChapterTable.getWithUserData(userId) .slice(ChapterTable.manga, ChapterUserTable.isRead.count()) - .select { (ChapterTable.manga inList ids) and (ChapterTable.isRead eq false) } + .select { (ChapterTable.manga inList ids) and (ChapterUserTable.isRead eq false) } .groupBy(ChapterTable.manga) - .associate { it[ChapterTable.manga].value to it[ChapterTable.isRead.count()] } + .associate { it[ChapterTable.manga].value to it[ChapterUserTable.isRead.count()] } ids.map { unreadChapterCountByMangaId[it]?.toInt() ?: 0 } } } @@ -111,8 +113,9 @@ class LastReadChapterForMangaDataLoader : KotlinDataLoader { override val dataLoaderName = "LastReadChapterForMangaDataLoader" override fun getDataLoader(): DataLoader = - DataLoaderFactory.newDataLoader { ids -> + DataLoaderFactory.newDataLoader { ids, env -> future { + val userId = env.getAttribute(JavalinSetup.Attribute.TachideskUser).requireUser() transaction { addLogger(Slf4jSqlDebugLogger) val lastReadChaptersByMangaId = 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 4e82c6134..d5eabda83 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/DownloadMutation.kt @@ -326,10 +326,10 @@ class DownloadMutation { dataFetchingEnvironment: DataFetchingEnvironment, input: DownloadAheadInput, ): DownloadAheadPayload { - dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() + val userId = dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser() val (clientMutationId, mangaIds, latestReadChapterIds) = input - Manga.downloadAhead(mangaIds, latestReadChapterIds ?: emptyList()) + Manga.downloadAhead(userId, mangaIds, latestReadChapterIds ?: emptyList()) return DownloadAheadPayload(clientMutationId) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index ad87c27e6..b581abf9f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -218,13 +218,19 @@ object Chapter { } } - if (manga.inLibrary) { + val isInALibrary = + transaction { + MangaUserTable.select { MangaUserTable.manga eq mangaId and (MangaUserTable.inLibrary eq true) }.isNotEmpty() + } + + if (isInALibrary) { downloadNewChapters(mangaId, numberOfCurrentChapters, newChapters) } return chapterList } + // todo user accounts private fun downloadNewChapters( mangaId: Int, prevNumberOfChapters: Int, @@ -252,7 +258,7 @@ object Chapter { updatedChapterList.indexOfFirst { it.getOrNull(ChapterUserTable.isRead) == true }.takeIf { it > -1 } ?: return val unreadChapters = updatedChapterList.subList(numberOfNewChapters, latestReadChapterIndex) - .filter { !it[ChapterTable.isRead] } + .filter { it.getOrNull(ChapterUserTable.isRead) != true } val skipDueToUnreadChapters = serverConfig.excludeEntryWithUnreadChapters.value && unreadChapters.isNotEmpty() if (skipDueToUnreadChapters) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt index 2ecff28d6..f98b1c91e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Manga.kt @@ -371,6 +371,7 @@ object Manga { private const val CHAPTERS_KEY = "chapterIds" fun downloadAhead( + userId: Int, mangaIds: List, latestReadChapterIds: List = emptyList(), ) { @@ -396,6 +397,7 @@ object Manga { object : TimerTask() { override fun run() { downloadAheadChapters( + userId, downloadAheadQueue[MANGAS_KEY]?.toList().orEmpty(), downloadAheadQueue[CHAPTERS_KEY]?.toList().orEmpty(), ) @@ -426,18 +428,23 @@ object Manga { * will download the unread chapters starting from chapter 15 */ private fun downloadAheadChapters( + userId: Int, mangaIds: List, latestReadChapterIds: List, ) { val mangaToLatestReadChapterIndex = transaction { - ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq true) } - .orderBy(ChapterTable.sourceOrder to SortOrder.DESC).groupBy { it[ChapterTable.manga].value } + ChapterTable.getWithUserData(userId).select { + (ChapterTable.manga inList mangaIds) and (ChapterUserTable.isRead eq true) + } + .orderBy(ChapterTable.sourceOrder to SortOrder.DESC) + .groupBy { it[ChapterTable.manga].value } }.mapValues { (_, chapters) -> chapters.firstOrNull()?.let { it[ChapterTable.sourceOrder] } ?: 0 } val mangaToUnreadChaptersMap = transaction { - ChapterTable.select { (ChapterTable.manga inList mangaIds) and (ChapterTable.isRead eq false) } + ChapterTable.getWithUserData(userId) + .select { (ChapterTable.manga inList mangaIds) and (ChapterUserTable.isRead eq false) } .orderBy(ChapterTable.sourceOrder to SortOrder.DESC) .groupBy { it[ChapterTable.manga].value } }