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

@@ -3,16 +3,21 @@ package suwayomi.tachidesk.graphql.mutations
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.TrackRecordType
import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
class TrackMutation {
@@ -28,7 +33,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture<LoginTrackerOAuthPayload> {
fun loginTrackerOAuth(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LoginTrackerOAuthInput,
): CompletableFuture<LoginTrackerOAuthPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -57,7 +66,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture<LoginTrackerCredentialsPayload> {
fun loginTrackerCredentials(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LoginTrackerCredentialsInput,
): CompletableFuture<LoginTrackerCredentialsPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -84,7 +97,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun logoutTracker(input: LogoutTrackerInput): CompletableFuture<LogoutTrackerPayload> {
fun logoutTracker(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LogoutTrackerInput,
): CompletableFuture<LogoutTrackerPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -117,7 +134,11 @@ class TrackMutation {
val trackRecord: TrackRecordType,
)
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
fun bindTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: BindTrackInput,
): CompletableFuture<BindTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId, trackerId, remoteId, private) = input
return future {
@@ -152,7 +173,11 @@ class TrackMutation {
val trackRecord: TrackRecordType,
)
fun fetchTrack(input: FetchTrackInput): CompletableFuture<FetchTrackPayload> {
fun fetchTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchTrackInput,
): CompletableFuture<FetchTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, recordId) = input
return future {
@@ -184,7 +209,11 @@ class TrackMutation {
val trackRecord: TrackRecordType?,
)
fun unbindTrack(input: UnbindTrackInput): CompletableFuture<UnbindTrackPayload> {
fun unbindTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UnbindTrackInput,
): CompletableFuture<UnbindTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, recordId, deleteRemoteTrack) = input
return future {
@@ -214,7 +243,11 @@ class TrackMutation {
val trackRecords: List<TrackRecordType>,
)
fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
fun trackProgress(
dataFetchingEnvironment: DataFetchingEnvironment,
input: TrackProgressInput,
): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId) = input
return future {
@@ -256,8 +289,12 @@ class TrackMutation {
val trackRecord: TrackRecordType?,
)
fun updateTrack(input: UpdateTrackInput): CompletableFuture<UpdateTrackPayload> =
fun updateTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateTrackInput,
): CompletableFuture<UpdateTrackPayload> =
future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
Track.update(
Track.UpdateInput(
input.recordId,