Basic JWT implementation (#1524)

* Basic JWT implementation

* Move JWT to UI_LOGIN mode and bring back SIMPLE_LOGIN as before

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Refresh: Update only access token

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Implement JWT Audience

* Store JWT key

Generates the key on startup if not set

* Handle invalid Base64

* Make JWT expiry configurable

* Missing value parse

* Update server/src/main/kotlin/suwayomi/tachidesk/global/impl/util/Jwt.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Simplify Duration parsing

* JWT Protect Mutations

* JWT Protect Queries and Subscriptions

* JWT Protect v1 WebSockets

* WebSockets allow sending token via protocol header

* Also respect the `suwayomi-server-token` cookie

* JWT reduce default token expiry

* JWT Support cookie on WebSocket as well

* Lint

* Authenticate graphql subscription via connection_init payload

* WebView: Prefer explicit token over cookie

This hack was implemented because WebView sent `"null"` if no token was
supplied, just don't send a bad token, then we can do this properly

* WebView: Implement basic login dialog if no token supplied

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-08-21 00:04:48 +02:00
committed by GitHub
parent d90bfb6e3e
commit 8547159eec
60 changed files with 1567 additions and 410 deletions

View File

@@ -22,6 +22,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.server.primitives.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -39,14 +40,20 @@ import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.manga.model.table.insertAll
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
class TrackQuery {
fun tracker(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Int,
): CompletableFuture<TrackerType> = dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
): CompletableFuture<TrackerType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", id)
}
enum class TrackerOrderBy {
ID,
@@ -115,6 +122,7 @@ class TrackQuery {
)
fun trackers(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: TrackerCondition? = null,
@GraphQLDeprecated(
"Replaced with order",
@@ -133,6 +141,7 @@ class TrackQuery {
last: Int? = null,
offset: Int? = null,
): TrackerNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (queryResults, resultsAsType) =
run {
var res = TrackerManager.services.map { TrackerType(it) }
@@ -240,8 +249,10 @@ class TrackQuery {
fun trackRecord(
dataFetchingEnvironment: DataFetchingEnvironment,
id: Int,
): CompletableFuture<TrackRecordType> =
dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
): CompletableFuture<TrackRecordType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader<Int, TrackRecordType>("TrackRecordDataLoader", id)
}
enum class TrackRecordOrderBy(
override val column: Column<*>,
@@ -389,6 +400,7 @@ class TrackQuery {
}
fun trackRecords(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: TrackRecordCondition? = null,
filter: TrackRecordFilter? = null,
@GraphQLDeprecated(
@@ -408,6 +420,7 @@ class TrackQuery {
last: Int? = null,
offset: Int? = null,
): TrackRecordNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults =
transaction {
val res = TrackRecordTable.selectAll()
@@ -490,8 +503,12 @@ class TrackQuery {
val trackSearches: List<TrackSearchType>,
)
fun searchTracker(input: SearchTrackerInput): CompletableFuture<SearchTrackerPayload> =
fun searchTracker(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SearchTrackerInput,
): CompletableFuture<SearchTrackerPayload> =
future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Tracker not found"