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

@@ -154,6 +154,9 @@ cronUtils = "com.cronutils:cron-utils:9.2.1"
# Webview # Webview
kcef = "dev.datlag:kcef:2024.04.20.4" kcef = "dev.datlag:kcef:2024.04.20.4"
# User
jwt = "com.auth0:java-jwt:4.4.0"
# lint - used for renovate to update ktlint version # lint - used for renovate to update ktlint version
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" } ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }

View File

@@ -102,6 +102,8 @@ dependencies {
implementation(libs.cronUtils) implementation(libs.cronUtils)
implementation(libs.jwt)
compileOnly(libs.kte) compileOnly(libs.kte)
} }

View File

@@ -142,6 +142,7 @@
<string name="webview_label_loading">Loading page...</string> <string name="webview_label_loading">Loading page...</string>
<string name="webview_label_copy">Copy to Clipboard</string> <string name="webview_label_copy">Copy to Clipboard</string>
<string name="webview_label_copy_description">Automatic clipboard copy failed, please use the input below to manually copy the value.</string> <string name="webview_label_copy_description">Automatic clipboard copy failed, please use the input below to manually copy the value.</string>
<string name="webview_label_login_required">Your configuration requires you to login. Please enter username and password.</string>
<string name="webview_placeholder_url">Enter URL...</string> <string name="webview_placeholder_url">Enter URL...</string>
<string name="login_label_title">Suwayomi Login</string> <string name="login_label_title">Suwayomi Login</string>

View File

@@ -159,25 +159,26 @@
main .contextmenu button:hover { main .contextmenu button:hover {
background: #eee; background: #eee;
} }
.copydialog { .copydialog, .logindialog {
display: none; display: none;
position: absolute; position: absolute;
inset: 0; inset: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 6px; padding: 6px;
z-index: 1;
} }
.copydialog.show { .copydialog.show, .logindialog.show {
display: block; display: block;
} }
.copydialog::before { .copydialog::before, .logindialog::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
background: black; background: black;
opacity: 0.3; opacity: 0.3;
} }
.copydialog__inner { .copydialog__inner, .logindialog__inner {
position: relative; position: relative;
max-width: 960px; max-width: 960px;
border-radius: 8px; border-radius: 8px;
@@ -204,10 +205,10 @@
line-height: 1; line-height: 1;
} }
@media (min-width: 500px) { @media (min-width: 500px) {
.copydialog { .copydialog, .logindialog {
padding: 24px; padding: 24px;
} }
.copydialog__inner { .copydialog__inner, .logindialog__inner {
padding: 12px 18px; padding: 12px 18px;
height: auto; height: auto;
} }
@@ -222,6 +223,86 @@
border-bottom: 9px solid transparent; border-bottom: 9px solid transparent;
border-left: 9px solid currentcolor; border-left: 9px solid currentcolor;
} }
.logindialog .error {
margin: 8px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #b71c1c;
background-color: #c62828;
color: white;
}
.logindialog .error:empty {
display: none;
}
.logindialog form label {
cursor: pointer;
}
.logindialog form button {
all: unset;
padding: 8px;
line-height: 1.75;
text-align: center;
min-width: 64px;
border-radius: 4px;
padding: 6px 8px;
color: rgb(91, 116, 239);
text-transform: uppercase;
letter-spacing: 0.02857em;
}
.logindialog form button:not([disabled]) {
cursor: pointer;
}
.logindialog form button:not([disabled]):hover {
background-color: rgba(91, 116, 239, 0.08);
}
.logindialog form input {
all: unset;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.23);
padding: 6px 12px;
width: auto;
min-width: 0;
}
.logindialog form input:hover {
border-color: white;
}
.logindialog form input:focus {
border-color: rgb(91, 116, 239);
}
.logindialog form .controls {
display: grid;
align-items: center;
grid-template-columns: 1fr;
}
.logindialog form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 6px;
}
.logindialog form .submit {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 24px;
}
.logindialog input:disabled, .logindialog button:disabled {
opacity: 0.7;
}
@media (min-width: 500px) {
.logindialog form {
width: 100%;
max-width: 450px;
margin: 8px auto;
}
.logindialog form .controls {
grid-template-columns: auto 1fr;
column-gap: 16px;
row-gap: 6px;
}
.logindialog form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 0px;
}
}
</style> </style>
</head> </head>
<body> <body>
@@ -256,6 +337,24 @@
<input type="text" id="copyinput" disabled readonly/> <input type="text" id="copyinput" disabled readonly/>
</div> </div>
</div> </div>
<div class="logindialog" id="logindialog" role="dialog">
<div class="logindialog__inner">
<form>
<h2>Login</h2>
<div class="error"></div>
<p>${MR.strings.webview_label_login_required.localized(locale)}</p>
<div class="controls">
<label for="user">${MR.strings.login_label_username.localized(locale)}:</label>
<input type="text" name="user" id="user" required placeholder="${MR.strings.login_placeholder_username.localized(locale)}"/>
<label for="pass">${MR.strings.login_label_password.localized(locale)}:</label>
<input type="password" name="pass" id="pass" required placeholder="${MR.strings.login_placeholder_password.localized(locale)}"/>
</div>
<div class="submit">
<button type="submit" disabled>${MR.strings.login_label_login.localized(locale)}</button>
</div>
</form>
</div>
</div>
<script> <script>
const messageDiv = document.getElementById('message'); const messageDiv = document.getElementById('message');
const statusDiv = document.getElementById('status'); const statusDiv = document.getElementById('status');
@@ -274,10 +373,89 @@
const titleDiv = document.getElementById('title'); const titleDiv = document.getElementById('title');
const reverseToggle = document.getElementById('reverseScroll'); const reverseToggle = document.getElementById('reverseScroll');
const origTitle = document.title; const origTitle = document.title;
const params = new URLSearchParams(window.location.search);
const token = params.get('token');
function connectWs(socketUrl, token) {
return new Promise((resolve, reject) => {
// we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
// browsers don't support setting custom headers for WebSockets...
const socket = new WebSocket(socketUrl, token ? [token] : []);
const f = (msg) => {
console.debug('Connection active:', msg.data);
socket.removeEventListener('message', f);
socket.removeEventListener('close', closef);
resolve(socket);
};
const closef = (e) => {
socket.removeEventListener('message', f);
if (e.code === 1011 && e.reason === "Unauthorized") {
const loginDiv = document.getElementById('logindialog');
const loginForm = document.querySelector('#logindialog form');
const loginError = document.querySelector('#logindialog .error');
loginError.textContent = '';
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
loginForm.addEventListener('submit', async (sev) => {
sev.preventDefault();
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
const mutation = {
"query": "mutation LOGIN($input: LoginInput!) {\n login(input: $input) {\n accessToken\n refreshToken\n }\n}",
"variables": {
"input": {
"username": loginForm.user.value,
"password": loginForm.pass.value,
},
},
"operationName": "LOGIN",
};
const resp = await fetch("/api/graphql", {
"headers": {
"Accept": "application/json, multipart/mixed",
"Content-Type": "application/json",
"Cache-Control": "no-cache"
},
"body": JSON.stringify(mutation),
"method": "POST",
}).then(r => r.json());
if (resp.errors && resp.errors.length > 0) {
const err = resp.errors[0].message.replace(/Exception[^:]* :|\r?\n.*/g, '');
loginError.textContent = err;
loginForm.pass.value = '';
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = false);
} else {
const newToken = resp.data.login.accessToken;
const expiry = new Date(JSON.parse(atob(newToken.split('.')[1])).exp * 1000);
console.log('Got new token', newToken, 'expires', expiry);
document.cookie = "suwayomi-server-token=" + newToken + "; path=/; expires=" + expiry.toUTCString();
loginDiv.classList.remove('show');
loginForm.querySelectorAll('input, button').forEach(i => i.disabled = true);
const newSocket = new WebSocket(socketUrl, [newToken]);
newSocket.addEventListener('open', () => {
resolve(newSocket);
});
}
});
loginDiv.classList.add('show');
return;
}
socket.removeEventListener('close', closef);
reject(e);
};
socket.addEventListener('open', () => {
console.debug('Socket opened, PING');
socket.addEventListener('message', f);
socket.send(JSON.stringify({type: "ping"}));
});
socket.addEventListener('close', closef);
});
}
(async function() {
try { try {
const socketUrl = (window.location.origin + window.location.pathname).replace(/^http/,'ws'); const socketUrl = (window.location.origin + window.location.pathname).replace(/^http/,'ws');
const socket = new WebSocket(socketUrl); // we pass the token as the subprotocol, which is widely considered the best solution to passing tokens
// browsers don't support setting custom headers for WebSockets...
const socket = await connectWs(socketUrl, token);
urlInput.disabled = false; urlInput.disabled = false;
goButton.disabled = false; goButton.disabled = false;
@@ -393,11 +571,6 @@
/// Server events /// Server events
socket.addEventListener('open', () => {
loadUrl(url);
console.log('WebSocket connection opened');
});
socket.addEventListener('message', e => { socket.addEventListener('message', e => {
const obj = JSON.parse(e.data); const obj = JSON.parse(e.data);
switch (obj.type) { switch (obj.type) {
@@ -449,7 +622,7 @@
if (e.wasClean) { if (e.wasClean) {
console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason); console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason);
} else { } else {
console.error('WebSocket connection died'); console.error('WebSocket connection died', e);
} }
document.body.classList.add('disconnected'); document.body.classList.add('disconnected');
}); });
@@ -583,11 +756,13 @@
}; };
attachEvents(); attachEvents();
frameInput.focus(); frameInput.focus();
loadUrl(url);
} catch (e) { } catch (e) {
messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e); messageDiv.textContent = "${MR.strings.label_error.localized(locale)}: " + (e.message || e.reason || e);
messageDiv.classList.add('error'); messageDiv.classList.add('error');
console.error(e); console.error(e);
} }
})();
</script> </script>
</body> </body>
</html> </html>

View File

@@ -9,6 +9,9 @@ package suwayomi.tachidesk.global.controller
import io.javalin.http.HttpStatus import io.javalin.http.HttpStatus
import suwayomi.tachidesk.global.impl.GlobalMeta 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.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -24,6 +27,7 @@ object GlobalMetaController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(GlobalMeta.getMetaMap()) ctx.json(GlobalMeta.getMetaMap())
ctx.status(200) ctx.status(200)
}, },
@@ -44,6 +48,7 @@ object GlobalMetaController {
} }
}, },
behaviorOf = { ctx, key, value -> behaviorOf = { ctx, key, value ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
GlobalMeta.modifyMeta(key, value) GlobalMeta.modifyMeta(key, value)
ctx.status(200) ctx.status(200)
}, },

View File

@@ -12,7 +12,10 @@ import suwayomi.tachidesk.global.impl.About
import suwayomi.tachidesk.global.impl.AboutDataClass import suwayomi.tachidesk.global.impl.AboutDataClass
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.global.impl.UpdateDataClass import suwayomi.tachidesk.global.impl.UpdateDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.handler
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -28,6 +31,7 @@ object SettingsController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(About.getAbout()) ctx.json(About.getAbout())
}, },
withResults = { withResults = {
@@ -45,6 +49,7 @@ object SettingsController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { AppUpdate.checkUpdate() } future { AppUpdate.checkUpdate() }
.thenApply { ctx.json(it) } .thenApply { ctx.json(it) }

View File

@@ -12,6 +12,9 @@ import io.javalin.http.HttpStatus
import io.javalin.websocket.WsConfig import io.javalin.websocket.WsConfig
import suwayomi.tachidesk.global.impl.WebView import suwayomi.tachidesk.global.impl.WebView
import suwayomi.tachidesk.i18n.LocalizationHelper import suwayomi.tachidesk.i18n.LocalizationHelper
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -28,6 +31,7 @@ object WebViewController {
} }
}, },
behaviorOf = { ctx, lang -> behaviorOf = { ctx, lang ->
// intentionally not user-protected, this pages handles login by itself
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.contentType(ContentType.TEXT_HTML) ctx.contentType(ContentType.TEXT_HTML)
ctx.render( ctx.render(
@@ -41,8 +45,15 @@ object WebViewController {
) )
fun webviewWS(ws: WsConfig) { fun webviewWS(ws: WsConfig) {
ws.onConnect { ctx -> WebView.addClient(ctx) } ws.onConnect { ctx ->
ws.onMessage { ctx -> WebView.handleRequest(ctx) } ctx.getAttribute(Attribute.TachideskUser).requireUser()
ws.onClose { ctx -> WebView.removeClient(ctx) } WebView.addClient(ctx)
}
ws.onMessage { ctx ->
WebView.handleRequest(ctx)
}
ws.onClose { ctx ->
WebView.removeClient(ctx)
}
} }
} }

View File

@@ -89,6 +89,10 @@ object WebView : Websocket<String>() {
@SerialName("copy") @SerialName("copy")
class JsCopyMessage : TypeObject() class JsCopyMessage : TypeObject()
@Serializable
@SerialName("ping")
class JsPingMessage : TypeObject()
override fun handleRequest(ctx: WsMessageContext) { override fun handleRequest(ctx: WsMessageContext) {
val dr = driver ?: return val dr = driver ?: return
try { try {
@@ -113,6 +117,9 @@ object WebView : Websocket<String>() {
is JsCopyMessage -> { is JsCopyMessage -> {
dr.copy() dr.copy()
} }
is JsPingMessage -> {
notifyAllClients("{\"type\":\"pong\"}")
}
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" } logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" }

View File

@@ -0,0 +1,126 @@
package suwayomi.tachidesk.global.impl.util
import android.app.Application
import android.content.Context
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException
import io.github.oshai.kotlinlogging.KotlinLogging
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.user.UserType
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.security.SecureRandom
import java.time.Instant
import javax.crypto.spec.SecretKeySpec
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
object Jwt {
private val preferenceStore =
Injekt.get<Application>().getSharedPreferences("jwt", Context.MODE_PRIVATE)
private val logger = KotlinLogging.logger {}
private const val ALGORITHM = "HmacSHA256"
private val accessTokenExpiry get() = serverConfig.jwtTokenExpiry.value
private val refreshTokenExpiry get() = serverConfig.jwtRefreshExpiry.value
private const val ISSUER = "suwayomi-server"
private val AUDIENCE get() = serverConfig.jwtAudience.value
private const val PREF_KEY = "jwt_key"
@OptIn(ExperimentalEncodingApi::class)
fun generateSecret(): String {
val byteString = preferenceStore.getString(PREF_KEY, "")
val decodedKeyBytes =
try {
Base64.Default.decode(byteString)
} catch (e: IllegalArgumentException) {
logger.warn(e) { "Invalid key specified, regenerating" }
null
}
val keyBytes =
if (decodedKeyBytes?.size == 32) {
decodedKeyBytes
} else {
val k = ByteArray(32)
SecureRandom().nextBytes(k)
preferenceStore.edit().putString(PREF_KEY, Base64.Default.encode(k)).apply()
k
}
val secretKey = SecretKeySpec(keyBytes, ALGORITHM)
return Base64.encode(secretKey.encoded)
}
private val algorithm: Algorithm = Algorithm.HMAC256(generateSecret())
private val verifier: JWTVerifier = JWT.require(algorithm).build()
class JwtTokens(
val accessToken: String,
val refreshToken: String,
)
fun generateJwt(): JwtTokens {
val accessToken = createAccessToken()
val refreshToken = createRefreshToken()
return JwtTokens(
accessToken = accessToken,
refreshToken = refreshToken,
)
}
fun refreshJwt(refreshToken: String): String {
val jwt = verifier.verify(refreshToken)
require(jwt.getClaim("token_type").asString() == "refresh") {
"Cannot use access token to refresh"
}
require(jwt.audience.single() == AUDIENCE) {
"Token intended for different audience ${jwt.audience}"
}
return createAccessToken()
}
fun verifyJwt(jwt: String): UserType {
try {
val decodedJWT = verifier.verify(jwt)
require(decodedJWT.getClaim("token_type").asString() == "access") {
"Cannot use refresh token to access"
}
require(decodedJWT.audience.single() == AUDIENCE) {
"Token intended for different audience ${decodedJWT.audience}"
}
return UserType.Admin(1)
} catch (e: JWTVerificationException) {
logger.warn(e) { "Received invalid token" }
return UserType.Visitor
}
}
private fun createAccessToken(): String {
val jwt =
JWT
.create()
.withIssuer(ISSUER)
.withAudience(AUDIENCE)
.withClaim("token_type", "access")
.withExpiresAt(Instant.now().plusSeconds(accessTokenExpiry.inWholeSeconds))
return jwt.sign(algorithm)
}
private fun createRefreshToken(): String =
JWT
.create()
.withIssuer(ISSUER)
.withAudience(AUDIENCE)
.withClaim("token_type", "refresh")
.withExpiresAt(Instant.now().plusSeconds(refreshTokenExpiry.inWholeSeconds))
.sign(algorithm)
}

View File

@@ -1,16 +1,21 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -26,7 +31,11 @@ class BackupMutation {
val status: BackupRestoreStatus?, val status: BackupRestoreStatus?,
) )
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> { fun restoreBackup(
dataFetchingEnvironment: DataFetchingEnvironment,
input: RestoreBackupInput,
): CompletableFuture<RestoreBackupPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, backup) = input val (clientMutationId, backup) = input
return future { return future {
@@ -53,7 +62,11 @@ class BackupMutation {
val url: String, val url: String,
) )
fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload { fun createBackup(
dataFetchingEnvironment: DataFetchingEnvironment,
input: CreateBackupInput? = null,
): CreateBackupPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val filename = Backup.getFilename() val filename = Backup.getFilename()
val backup = val backup =

View File

@@ -1,6 +1,7 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
@@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.CategoryMetaType import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
@@ -23,6 +25,9 @@ import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
class CategoryMutation { class CategoryMutation {
data class SetCategoryMetaInput( data class SetCategoryMetaInput(
@@ -35,8 +40,12 @@ class CategoryMutation {
val meta: CategoryMetaType, val meta: CategoryMetaType,
) )
fun setCategoryMeta(input: SetCategoryMetaInput): DataFetcherResult<SetCategoryMetaPayload?> = fun setCategoryMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetCategoryMetaInput,
): DataFetcherResult<SetCategoryMetaPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Category.modifyMeta(meta.categoryId, meta.key, meta.value) Category.modifyMeta(meta.categoryId, meta.key, meta.value)
@@ -56,8 +65,12 @@ class CategoryMutation {
val category: CategoryType, val category: CategoryType,
) )
fun deleteCategoryMeta(input: DeleteCategoryMetaInput): DataFetcherResult<DeleteCategoryMetaPayload?> = fun deleteCategoryMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteCategoryMetaInput,
): DataFetcherResult<DeleteCategoryMetaPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, categoryId, key) = input val (clientMutationId, categoryId, key) = input
val (meta, category) = val (meta, category) =
@@ -150,8 +163,12 @@ class CategoryMutation {
} }
} }
fun updateCategory(input: UpdateCategoryInput): DataFetcherResult<UpdateCategoryPayload?> = fun updateCategory(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateCategoryInput,
): DataFetcherResult<UpdateCategoryPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
updateCategories(listOf(id), patch) updateCategories(listOf(id), patch)
@@ -167,8 +184,12 @@ class CategoryMutation {
) )
} }
fun updateCategories(input: UpdateCategoriesInput): DataFetcherResult<UpdateCategoriesPayload?> = fun updateCategories(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateCategoriesInput,
): DataFetcherResult<UpdateCategoriesPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
updateCategories(ids, patch) updateCategories(ids, patch)
@@ -195,8 +216,12 @@ class CategoryMutation {
val position: Int, val position: Int,
) )
fun updateCategoryOrder(input: UpdateCategoryOrderInput): DataFetcherResult<UpdateCategoryOrderPayload?> = fun updateCategoryOrder(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateCategoryOrderInput,
): DataFetcherResult<UpdateCategoryOrderPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, categoryId, position) = input val (clientMutationId, categoryId, position) = input
require(position > 0) { require(position > 0) {
"'order' must not be <= 0" "'order' must not be <= 0"
@@ -253,8 +278,12 @@ class CategoryMutation {
val category: CategoryType, val category: CategoryType,
) )
fun createCategory(input: CreateCategoryInput): DataFetcherResult<CreateCategoryPayload?> = fun createCategory(
dataFetchingEnvironment: DataFetchingEnvironment,
input: CreateCategoryInput,
): DataFetcherResult<CreateCategoryPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input val (clientMutationId, name, order, default, includeInUpdate, includeInDownload) = input
transaction { transaction {
require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) { require(CategoryTable.selectAll().where { CategoryTable.name eq input.name }.isEmpty()) {
@@ -312,8 +341,12 @@ class CategoryMutation {
val mangas: List<MangaType>, val mangas: List<MangaType>,
) )
fun deleteCategory(input: DeleteCategoryInput): DataFetcherResult<DeleteCategoryPayload?> { fun deleteCategory(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteCategoryInput,
): DataFetcherResult<DeleteCategoryPayload?> {
return asDataFetcherResult { return asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, categoryId) = input val (clientMutationId, categoryId) = input
if (categoryId == 0) { // Don't delete default category if (categoryId == 0) { // Don't delete default category
return@asDataFetcherResult DeleteCategoryPayload( return@asDataFetcherResult DeleteCategoryPayload(
@@ -401,8 +434,12 @@ class CategoryMutation {
} }
} }
fun updateMangaCategories(input: UpdateMangaCategoriesInput): DataFetcherResult<UpdateMangaCategoriesPayload?> = fun updateMangaCategories(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateMangaCategoriesInput,
): DataFetcherResult<UpdateMangaCategoriesPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
updateMangas(listOf(id), patch) updateMangas(listOf(id), patch)
@@ -418,8 +455,12 @@ class CategoryMutation {
) )
} }
fun updateMangasCategories(input: UpdateMangasCategoriesInput): DataFetcherResult<UpdateMangasCategoriesPayload?> = fun updateMangasCategories(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateMangasCategoriesInput,
): DataFetcherResult<UpdateMangasCategoriesPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
updateMangas(ids, patch) updateMangas(ids, patch)

View File

@@ -1,6 +1,7 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
@@ -12,6 +13,7 @@ import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.ChapterMetaType
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.graphql.types.SyncConflictInfoType import suwayomi.tachidesk.graphql.types.SyncConflictInfoType
@@ -20,7 +22,10 @@ import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.net.URLEncoder import java.net.URLEncoder
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -112,8 +117,12 @@ class ChapterMutation {
} }
} }
fun updateChapter(input: UpdateChapterInput): DataFetcherResult<UpdateChapterPayload?> = fun updateChapter(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateChapterInput,
): DataFetcherResult<UpdateChapterPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
updateChapters(listOf(id), patch) updateChapters(listOf(id), patch)
@@ -129,8 +138,12 @@ class ChapterMutation {
) )
} }
fun updateChapters(input: UpdateChaptersInput): DataFetcherResult<UpdateChaptersPayload?> = fun updateChapters(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateChaptersInput,
): DataFetcherResult<UpdateChaptersPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
updateChapters(ids, patch) updateChapters(ids, patch)
@@ -156,7 +169,11 @@ class ChapterMutation {
val chapters: List<ChapterType>, val chapters: List<ChapterType>,
) )
fun fetchChapters(input: FetchChaptersInput): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> { fun fetchChapters(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchChaptersInput,
): CompletableFuture<DataFetcherResult<FetchChaptersPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId) = input val (clientMutationId, mangaId) = input
return future { return future {
@@ -190,8 +207,12 @@ class ChapterMutation {
val meta: ChapterMetaType, val meta: ChapterMetaType,
) )
fun setChapterMeta(input: SetChapterMetaInput): DataFetcherResult<SetChapterMetaPayload?> = fun setChapterMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetChapterMetaInput,
): DataFetcherResult<SetChapterMetaPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value) Chapter.modifyChapterMeta(meta.chapterId, meta.key, meta.value)
@@ -211,8 +232,12 @@ class ChapterMutation {
val chapter: ChapterType, val chapter: ChapterType,
) )
fun deleteChapterMeta(input: DeleteChapterMetaInput): DataFetcherResult<DeleteChapterMetaPayload?> = fun deleteChapterMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteChapterMetaInput,
): DataFetcherResult<DeleteChapterMetaPayload?> =
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapterId, key) = input val (clientMutationId, chapterId, key) = input
val (meta, chapter) = val (meta, chapter) =
@@ -260,7 +285,11 @@ class ChapterMutation {
val syncConflict: SyncConflictInfoType?, val syncConflict: SyncConflictInfoType?,
) )
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> { fun fetchChapterPages(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchChapterPagesInput,
): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapterId) = input val (clientMutationId, chapterId) = input
val paramsMap = input.toParams() val paramsMap = input.toParams()

View File

@@ -1,11 +1,13 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.Chapter
@@ -13,7 +15,10 @@ import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.download.model.DownloadUpdateType.DEQUEUED import suwayomi.tachidesk.manga.impl.download.model.DownloadUpdateType.DEQUEUED
import suwayomi.tachidesk.manga.impl.download.model.Status import suwayomi.tachidesk.manga.impl.download.model.Status
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -28,7 +33,11 @@ class DownloadMutation {
val chapters: List<ChapterType>, val chapters: List<ChapterType>,
) )
fun deleteDownloadedChapters(input: DeleteDownloadedChaptersInput): DataFetcherResult<DeleteDownloadedChaptersPayload?> { fun deleteDownloadedChapters(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteDownloadedChaptersInput,
): DataFetcherResult<DeleteDownloadedChaptersPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -57,7 +66,11 @@ class DownloadMutation {
val chapters: ChapterType, val chapters: ChapterType,
) )
fun deleteDownloadedChapter(input: DeleteDownloadedChapterInput): DataFetcherResult<DeleteDownloadedChapterPayload?> { fun deleteDownloadedChapter(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteDownloadedChapterInput,
): DataFetcherResult<DeleteDownloadedChapterPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -84,8 +97,10 @@ class DownloadMutation {
) )
fun enqueueChapterDownloads( fun enqueueChapterDownloads(
dataFetchingEnvironment: DataFetchingEnvironment,
input: EnqueueChapterDownloadsInput, input: EnqueueChapterDownloadsInput,
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> { ): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadsPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
return future { return future {
@@ -118,7 +133,11 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun enqueueChapterDownload(input: EnqueueChapterDownloadInput): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> { fun enqueueChapterDownload(
dataFetchingEnvironment: DataFetchingEnvironment,
input: EnqueueChapterDownloadInput,
): CompletableFuture<DataFetcherResult<EnqueueChapterDownloadPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
return future { return future {
@@ -151,8 +170,10 @@ class DownloadMutation {
) )
fun dequeueChapterDownloads( fun dequeueChapterDownloads(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DequeueChapterDownloadsInput, input: DequeueChapterDownloadsInput,
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> { ): CompletableFuture<DataFetcherResult<DequeueChapterDownloadsPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapters) = input val (clientMutationId, chapters) = input
return future { return future {
@@ -187,7 +208,11 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun dequeueChapterDownload(input: DequeueChapterDownloadInput): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> { fun dequeueChapterDownload(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DequeueChapterDownloadInput,
): CompletableFuture<DataFetcherResult<DequeueChapterDownloadPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapter) = input val (clientMutationId, chapter) = input
return future { return future {
@@ -221,9 +246,13 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun startDownloader(input: StartDownloaderInput): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> = fun startDownloader(
dataFetchingEnvironment: DataFetchingEnvironment,
input: StartDownloaderInput,
): CompletableFuture<DataFetcherResult<StartDownloaderPayload?>> =
future { future {
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.start() DownloadManager.start()
StartDownloaderPayload( StartDownloaderPayload(
@@ -249,9 +278,13 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun stopDownloader(input: StopDownloaderInput): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> = fun stopDownloader(
dataFetchingEnvironment: DataFetchingEnvironment,
input: StopDownloaderInput,
): CompletableFuture<DataFetcherResult<StopDownloaderPayload?>> =
future { future {
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.stop() DownloadManager.stop()
StopDownloaderPayload( StopDownloaderPayload(
@@ -277,9 +310,13 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun clearDownloader(input: ClearDownloaderInput): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> = fun clearDownloader(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ClearDownloaderInput,
): CompletableFuture<DataFetcherResult<ClearDownloaderPayload?>> =
future { future {
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.clear() DownloadManager.clear()
ClearDownloaderPayload( ClearDownloaderPayload(
@@ -307,7 +344,11 @@ class DownloadMutation {
val downloadStatus: DownloadStatus, val downloadStatus: DownloadStatus,
) )
fun reorderChapterDownload(input: ReorderChapterDownloadInput): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> { fun reorderChapterDownload(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ReorderChapterDownloadInput,
): CompletableFuture<DataFetcherResult<ReorderChapterDownloadPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, chapter, to) = input val (clientMutationId, chapter, to) = input
return future { return future {

View File

@@ -2,15 +2,20 @@ package suwayomi.tachidesk.graphql.mutations
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class ExtensionMutation { class ExtensionMutation {
@@ -73,7 +78,11 @@ class ExtensionMutation {
} }
} }
fun updateExtension(input: UpdateExtensionInput): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> { fun updateExtension(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateExtensionInput,
): CompletableFuture<DataFetcherResult<UpdateExtensionPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
return future { return future {
@@ -97,7 +106,11 @@ class ExtensionMutation {
} }
} }
fun updateExtensions(input: UpdateExtensionsInput): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> { fun updateExtensions(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateExtensionsInput,
): CompletableFuture<DataFetcherResult<UpdateExtensionsPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
return future { return future {
@@ -129,7 +142,11 @@ class ExtensionMutation {
val extensions: List<ExtensionType>, val extensions: List<ExtensionType>,
) )
fun fetchExtensions(input: FetchExtensionsInput): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> { fun fetchExtensions(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchExtensionsInput,
): CompletableFuture<DataFetcherResult<FetchExtensionsPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId) = input val (clientMutationId) = input
return future { return future {
@@ -163,8 +180,10 @@ class ExtensionMutation {
) )
fun installExternalExtension( fun installExternalExtension(
dataFetchingEnvironment: DataFetchingEnvironment,
input: InstallExternalExtensionInput, input: InstallExternalExtensionInput,
): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> { ): CompletableFuture<DataFetcherResult<InstallExternalExtensionPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, extensionFile) = input val (clientMutationId, extensionFile) = input
return future { return future {

View File

@@ -1,7 +1,12 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
private val applicationDirs: ApplicationDirs by injectLazy() private val applicationDirs: ApplicationDirs by injectLazy()
@@ -21,7 +26,11 @@ class ImageMutation {
val cachedPages: Boolean?, val cachedPages: Boolean?,
) )
fun clearCachedImages(input: ClearCachedImagesInput): ClearCachedImagesPayload { fun clearCachedImages(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ClearCachedImagesInput,
): ClearCachedImagesPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, downloadedThumbnails, cachedThumbnails, cachedPages) = input val (clientMutationId, downloadedThumbnails, cachedThumbnails, cachedPages) = input
val downloadedThumbnailsResult = val downloadedThumbnailsResult =

View File

@@ -1,15 +1,20 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
import suwayomi.tachidesk.graphql.types.UpdateState.ERROR import suwayomi.tachidesk.graphql.types.UpdateState.ERROR
import suwayomi.tachidesk.graphql.types.UpdateState.IDLE import suwayomi.tachidesk.graphql.types.UpdateState.IDLE
import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -24,9 +29,13 @@ class InfoMutation {
val updateStatus: WebUIUpdateStatus, val updateStatus: WebUIUpdateStatus,
) )
fun updateWebUI(input: WebUIUpdateInput): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> { fun updateWebUI(
dataFetchingEnvironment: DataFetchingEnvironment,
input: WebUIUpdateInput,
): CompletableFuture<DataFetcherResult<WebUIUpdatePayload?>> {
return future { return future {
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
withTimeout(30.seconds) { withTimeout(30.seconds) {
if (WebInterfaceManager.status.value.state === DOWNLOADING) { if (WebInterfaceManager.status.value.state === DOWNLOADING) {
return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value) return@withTimeout WebUIUpdatePayload(input.clientMutationId, WebInterfaceManager.status.value)
@@ -59,9 +68,10 @@ class InfoMutation {
} }
} }
fun resetWebUIUpdateStatus(): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> = fun resetWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<DataFetcherResult<WebUIUpdateStatus?>> =
future { future {
asDataFetcherResult { asDataFetcherResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
withTimeout(30.seconds) { withTimeout(30.seconds) {
val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING val isUpdateFinished = WebInterfaceManager.status.value.state != DOWNLOADING
if (!isUpdateFinished) { if (!isUpdateFinished) {

View File

@@ -1,10 +1,15 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload import suwayomi.tachidesk.graphql.types.KoSyncConnectPayload
import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload import suwayomi.tachidesk.graphql.types.LogoutKoSyncAccountPayload
import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.graphql.types.SettingsType
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class KoreaderSyncMutation { class KoreaderSyncMutation {
@@ -14,8 +19,12 @@ class KoreaderSyncMutation {
val password: String, val password: String,
) )
fun connectKoSyncAccount(input: ConnectKoSyncAccountInput): CompletableFuture<KoSyncConnectPayload> = fun connectKoSyncAccount(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ConnectKoSyncAccountInput,
): CompletableFuture<KoSyncConnectPayload> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val result = KoreaderSyncService.connect(input.username, input.password) val result = KoreaderSyncService.connect(input.username, input.password)
KoSyncConnectPayload( KoSyncConnectPayload(
@@ -31,8 +40,12 @@ class KoreaderSyncMutation {
val clientMutationId: String? = null, val clientMutationId: String? = null,
) )
fun logoutKoSyncAccount(input: LogoutKoSyncAccountInput): CompletableFuture<LogoutKoSyncAccountPayload> = fun logoutKoSyncAccount(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LogoutKoSyncAccountInput,
): CompletableFuture<LogoutKoSyncAccountPayload> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
KoreaderSyncService.logout() KoreaderSyncService.logout()
LogoutKoSyncAccountPayload( LogoutKoSyncAccountPayload(
clientMutationId = input.clientMutationId, clientMutationId = input.clientMutationId,

View File

@@ -1,6 +1,7 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
@@ -8,6 +9,7 @@ import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.MangaMetaType import suwayomi.tachidesk.graphql.types.MangaMetaType
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.impl.Library import suwayomi.tachidesk.manga.impl.Library
@@ -16,7 +18,10 @@ import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.model.table.MangaMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.time.Instant import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -90,7 +95,11 @@ class MangaMutation {
} }
} }
fun updateManga(input: UpdateMangaInput): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> { fun updateManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateMangaInput,
): CompletableFuture<DataFetcherResult<UpdateMangaPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id, patch) = input val (clientMutationId, id, patch) = input
return future { return future {
@@ -110,7 +119,11 @@ class MangaMutation {
} }
} }
fun updateMangas(input: UpdateMangasInput): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> { fun updateMangas(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateMangasInput,
): CompletableFuture<DataFetcherResult<UpdateMangasPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, ids, patch) = input val (clientMutationId, ids, patch) = input
return future { return future {
@@ -140,7 +153,11 @@ class MangaMutation {
val manga: MangaType, val manga: MangaType,
) )
fun fetchManga(input: FetchMangaInput): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> { fun fetchManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchMangaInput,
): CompletableFuture<DataFetcherResult<FetchMangaPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, id) = input val (clientMutationId, id) = input
return future { return future {
@@ -169,7 +186,11 @@ class MangaMutation {
val meta: MangaMetaType, val meta: MangaMetaType,
) )
fun setMangaMeta(input: SetMangaMetaInput): DataFetcherResult<SetMangaMetaPayload?> { fun setMangaMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetMangaMetaInput,
): DataFetcherResult<SetMangaMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -191,7 +212,11 @@ class MangaMutation {
val manga: MangaType, val manga: MangaType,
) )
fun deleteMangaMeta(input: DeleteMangaMetaInput): DataFetcherResult<DeleteMangaMetaPayload?> { fun deleteMangaMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteMangaMetaInput,
): DataFetcherResult<DeleteMangaMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId, key) = input val (clientMutationId, mangaId, key) = input
return asDataFetcherResult { return asDataFetcherResult {

View File

@@ -1,6 +1,7 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -8,7 +9,11 @@ import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.global.impl.GlobalMeta import suwayomi.tachidesk.global.impl.GlobalMeta
import suwayomi.tachidesk.global.model.table.GlobalMetaTable import suwayomi.tachidesk.global.model.table.GlobalMetaTable
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
class MetaMutation { class MetaMutation {
data class SetGlobalMetaInput( data class SetGlobalMetaInput(
@@ -21,7 +26,11 @@ class MetaMutation {
val meta: GlobalMetaType, val meta: GlobalMetaType,
) )
fun setGlobalMeta(input: SetGlobalMetaInput): DataFetcherResult<SetGlobalMetaPayload?> { fun setGlobalMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetGlobalMetaInput,
): DataFetcherResult<SetGlobalMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -41,7 +50,11 @@ class MetaMutation {
val meta: GlobalMetaType?, val meta: GlobalMetaType?,
) )
fun deleteGlobalMeta(input: DeleteGlobalMetaInput): DataFetcherResult<DeleteGlobalMetaPayload?> { fun deleteGlobalMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteGlobalMetaInput,
): DataFetcherResult<DeleteGlobalMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, key) = input val (clientMutationId, key) = input
return asDataFetcherResult { return asDataFetcherResult {

View File

@@ -1,14 +1,19 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.PartialSettingsType import suwayomi.tachidesk.graphql.types.PartialSettingsType
import suwayomi.tachidesk.graphql.types.Settings import suwayomi.tachidesk.graphql.types.Settings
import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.graphql.types.SettingsType
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.user.requireUser
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import java.io.File import java.io.File
@@ -226,7 +231,11 @@ class SettingsMutation {
updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance) updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance)
} }
fun setSettings(input: SetSettingsInput): SetSettingsPayload { fun setSettings(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetSettingsInput,
): SetSettingsPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, settings) = input val (clientMutationId, settings) = input
validateSettings(settings) validateSettings(settings)
@@ -244,7 +253,11 @@ class SettingsMutation {
val settings: SettingsType, val settings: SettingsType,
) )
fun resetSettings(input: ResetSettingsInput): ResetSettingsPayload { fun resetSettings(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ResetSettingsInput,
): ResetSettingsPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId) = input val (clientMutationId) = input
GlobalConfigManager.resetUserConfig() GlobalConfigManager.resetUserConfig()

View File

@@ -6,12 +6,14 @@ import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.FilterChange
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.Preference import suwayomi.tachidesk.graphql.types.Preference
@@ -25,7 +27,10 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable
import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class SourceMutation { class SourceMutation {
@@ -39,7 +44,11 @@ class SourceMutation {
val meta: SourceMetaType, val meta: SourceMetaType,
) )
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> { fun setSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetSourceMetaInput,
): DataFetcherResult<SetSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input val (clientMutationId, meta) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -61,7 +70,11 @@ class SourceMutation {
val source: SourceType?, val source: SourceType?,
) )
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> { fun deleteSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteSourceMetaInput,
): DataFetcherResult<DeleteSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, key) = input val (clientMutationId, sourceId, key) = input
return asDataFetcherResult { return asDataFetcherResult {
@@ -116,7 +129,11 @@ class SourceMutation {
val hasNextPage: Boolean, val hasNextPage: Boolean,
) )
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> { fun fetchSourceManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchSourceMangaInput,
): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, type, page, query, filters) = input val (clientMutationId, sourceId, type, page, query, filters) = input
return future { return future {
@@ -182,7 +199,11 @@ class SourceMutation {
val source: SourceType, val source: SourceType,
) )
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> { fun updateSourcePreference(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateSourcePreferenceInput,
): DataFetcherResult<UpdateSourcePreferencePayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, change) = input val (clientMutationId, sourceId, change) = input
return asDataFetcherResult { return asDataFetcherResult {

View File

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

View File

@@ -1,14 +1,19 @@
package suwayomi.tachidesk.graphql.mutations package suwayomi.tachidesk.graphql.mutations
import graphql.execution.DataFetcherResult import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.asDataFetcherResult import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -26,7 +31,11 @@ class UpdateMutation {
val updateStatus: LibraryUpdateStatus, val updateStatus: LibraryUpdateStatus,
) )
fun updateLibrary(input: UpdateLibraryInput): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> { fun updateLibrary(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateLibraryInput,
): CompletableFuture<DataFetcherResult<UpdateLibraryPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
updater.addCategoriesToUpdateQueue( updater.addCategoriesToUpdateQueue(
Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true }, Category.getCategoryList().filter { input.categories?.contains(it.id) ?: true },
clear = true, clear = true,
@@ -57,8 +66,12 @@ class UpdateMutation {
val updateStatus: UpdateStatus, val updateStatus: UpdateStatus,
) )
fun updateLibraryManga(input: UpdateLibraryMangaInput): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> { fun updateLibraryManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateLibraryMangaInput,
): CompletableFuture<DataFetcherResult<UpdateLibraryMangaPayload?>> {
updateLibrary( updateLibrary(
dataFetchingEnvironment,
UpdateLibraryInput( UpdateLibraryInput(
clientMutationId = input.clientMutationId, clientMutationId = input.clientMutationId,
categories = null, categories = null,
@@ -88,8 +101,12 @@ class UpdateMutation {
val updateStatus: UpdateStatus, val updateStatus: UpdateStatus,
) )
fun updateCategoryManga(input: UpdateCategoryMangaInput): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> { fun updateCategoryManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateCategoryMangaInput,
): CompletableFuture<DataFetcherResult<UpdateCategoryMangaPayload?>> {
updateLibrary( updateLibrary(
dataFetchingEnvironment,
UpdateLibraryInput( UpdateLibraryInput(
clientMutationId = input.clientMutationId, clientMutationId = input.clientMutationId,
categories = input.categories, categories = input.categories,
@@ -117,7 +134,11 @@ class UpdateMutation {
val clientMutationId: String?, val clientMutationId: String?,
) )
fun updateStop(input: UpdateStopInput): UpdateStopPayload { fun updateStop(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateStopInput,
): UpdateStopPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
updater.reset() updater.reset()
return UpdateStopPayload(input.clientMutationId) return UpdateStopPayload(input.clientMutationId)
} }

View File

@@ -0,0 +1,64 @@
package suwayomi.tachidesk.graphql.mutations
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.global.impl.util.Jwt
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.user.UserType
class UserMutation {
data class LoginInput(
val clientMutationId: String? = null,
val username: String,
val password: String,
)
data class LoginPayload(
val clientMutationId: String?,
val accessToken: String,
val refreshToken: String,
)
fun login(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LoginInput,
): LoginPayload {
if (dataFetchingEnvironment.getAttribute(Attribute.TachideskUser) !is UserType.Visitor) {
throw IllegalArgumentException("Cannot login while already logged-in")
}
val isValid =
input.username == serverConfig.authUsername.value &&
input.password == serverConfig.authPassword.value
if (isValid) {
val jwt = Jwt.generateJwt()
return LoginPayload(
clientMutationId = input.clientMutationId,
accessToken = jwt.accessToken,
refreshToken = jwt.refreshToken,
)
} else {
throw Exception("Incorrect username or password.")
}
}
data class RefreshTokenInput(
val clientMutationId: String? = null,
val refreshToken: String,
)
data class RefreshTokenPayload(
val clientMutationId: String?,
val accessToken: String,
)
fun refreshToken(input: RefreshTokenInput): RefreshTokenPayload {
val accessToken = Jwt.refreshJwt(input.refreshToken)
return RefreshTokenPayload(
clientMutationId = input.clientMutationId,
accessToken = accessToken,
)
}
}

View File

@@ -1,10 +1,15 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
import suwayomi.tachidesk.graphql.types.toStatus import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
class BackupQuery { class BackupQuery {
data class ValidateBackupInput( data class ValidateBackupInput(
@@ -25,7 +30,11 @@ class BackupQuery {
val missingTrackers: List<ValidateBackupTracker>, val missingTrackers: List<ValidateBackupTracker>,
) )
fun validateBackup(input: ValidateBackupInput): ValidateBackupResult { fun validateBackup(
dataFetchingEnvironment: DataFetchingEnvironment,
input: ValidateBackupInput,
): ValidateBackupResult {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val result = ProtoBackupValidator.validate(input.backup.content()) val result = ProtoBackupValidator.validate(input.backup.content())
return ValidateBackupResult( return ValidateBackupResult(
result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) }, result.missingSourceIds.map { ValidateBackupSource(it.first, it.second) },
@@ -33,5 +42,11 @@ class BackupQuery {
) )
} }
fun restoreStatus(id: String): BackupRestoreStatus? = ProtoBackupImport.getRestoreState(id)?.toStatus() fun restoreStatus(
dataFetchingEnvironment: DataFetchingEnvironment,
id: String,
): BackupRestoreStatus? {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return ProtoBackupImport.getRestoreState(id)?.toStatus()
}
} }

View File

@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.CategoryNodeList import suwayomi.tachidesk.graphql.types.CategoryNodeList
import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.manga.model.table.CategoryTable import suwayomi.tachidesk.manga.model.table.CategoryTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class CategoryQuery { class CategoryQuery {
fun category( fun category(
dataFetchingEnvironment: DataFetchingEnvironment, dataFetchingEnvironment: DataFetchingEnvironment,
id: Int, id: Int,
): CompletableFuture<CategoryType> = dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id) ): CompletableFuture<CategoryType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader("CategoryDataLoader", id)
}
enum class CategoryOrderBy( enum class CategoryOrderBy(
override val column: Column<*>, override val column: Column<*>,
@@ -121,6 +128,7 @@ class CategoryQuery {
} }
fun categories( fun categories(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: CategoryCondition? = null, condition: CategoryCondition? = null,
filter: CategoryFilter? = null, filter: CategoryFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -140,6 +148,7 @@ class CategoryQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): CategoryNodeList { ): CategoryNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults = val queryResults =
transaction { transaction {
val res = CategoryTable.selectAll() val res = CategoryTable.selectAll()

View File

@@ -30,6 +30,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -43,6 +44,9 @@ import suwayomi.tachidesk.graphql.types.ChapterNodeList
import suwayomi.tachidesk.graphql.types.ChapterType import suwayomi.tachidesk.graphql.types.ChapterType
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
/** /**
@@ -197,6 +201,7 @@ class ChapterQuery {
} }
fun chapters( fun chapters(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: ChapterCondition? = null, condition: ChapterCondition? = null,
filter: ChapterFilter? = null, filter: ChapterFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -216,6 +221,7 @@ class ChapterQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): ChapterNodeList { ): ChapterNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults = val queryResults =
transaction { transaction {
val res = ChapterTable.selectAll() val res = ChapterTable.selectAll()

View File

@@ -1,13 +1,19 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class DownloadQuery { class DownloadQuery {
fun downloadStatus(): CompletableFuture<DownloadStatus> = fun downloadStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<DownloadStatus> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
DownloadStatus(DownloadManager.getStatus()) DownloadStatus(DownloadManager.getStatus())
} }
} }

View File

@@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -40,13 +41,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.ExtensionNodeList import suwayomi.tachidesk.graphql.types.ExtensionNodeList
import suwayomi.tachidesk.graphql.types.ExtensionType import suwayomi.tachidesk.graphql.types.ExtensionType
import suwayomi.tachidesk.manga.model.table.ExtensionTable import suwayomi.tachidesk.manga.model.table.ExtensionTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class ExtensionQuery { class ExtensionQuery {
fun extension( fun extension(
dataFetchingEnvironment: DataFetchingEnvironment, dataFetchingEnvironment: DataFetchingEnvironment,
pkgName: String, pkgName: String,
): CompletableFuture<ExtensionType> = dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName) ): CompletableFuture<ExtensionType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader("ExtensionDataLoader", pkgName)
}
enum class ExtensionOrderBy( enum class ExtensionOrderBy(
override val column: Column<*>, override val column: Column<*>,
@@ -153,6 +160,7 @@ class ExtensionQuery {
} }
fun extensions( fun extensions(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: ExtensionCondition? = null, condition: ExtensionCondition? = null,
filter: ExtensionFilter? = null, filter: ExtensionFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -172,6 +180,7 @@ class ExtensionQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): ExtensionNodeList { ): ExtensionNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults = val queryResults =
transaction { transaction {
val res = ExtensionTable.selectAll() val res = ExtensionTable.selectAll()

View File

@@ -1,14 +1,19 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.global.impl.AppUpdate import suwayomi.tachidesk.global.impl.AppUpdate
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.AboutWebUI import suwayomi.tachidesk.graphql.types.AboutWebUI
import suwayomi.tachidesk.graphql.types.WebUIFlavor import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck import suwayomi.tachidesk.graphql.types.WebUIUpdateCheck
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -42,8 +47,9 @@ class InfoQuery {
val url: String, val url: String,
) )
fun checkForServerUpdates(): CompletableFuture<List<CheckForServerUpdatesPayload>> = fun checkForServerUpdates(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<CheckForServerUpdatesPayload>> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
AppUpdate.checkUpdate().map { AppUpdate.checkUpdate().map {
CheckForServerUpdatesPayload( CheckForServerUpdatesPayload(
channel = it.channel, channel = it.channel,
@@ -53,13 +59,15 @@ class InfoQuery {
} }
} }
fun aboutWebUI(): CompletableFuture<AboutWebUI> = fun aboutWebUI(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<AboutWebUI> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
WebInterfaceManager.getAboutInfo() WebInterfaceManager.getAboutInfo()
} }
fun checkForWebUIUpdate(): CompletableFuture<WebUIUpdateCheck> = fun checkForWebUIUpdate(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<WebUIUpdateCheck> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true) val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable(WebUIFlavor.current, raiseError = true)
WebUIUpdateCheck( WebUIUpdateCheck(
channel = serverConfig.webUIChannel.value, channel = serverConfig.webUIChannel.value,
@@ -68,5 +76,8 @@ class InfoQuery {
) )
} }
fun getWebUIUpdateStatus(): WebUIUpdateStatus = WebInterfaceManager.status.value fun getWebUIUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): WebUIUpdateStatus {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return WebInterfaceManager.status.value
}
} }

View File

@@ -1,13 +1,19 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload import suwayomi.tachidesk.graphql.types.KoSyncStatusPayload
import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService import suwayomi.tachidesk.manga.impl.sync.KoreaderSyncService
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class KoreaderSyncQuery { class KoreaderSyncQuery {
fun koSyncStatus(): CompletableFuture<KoSyncStatusPayload> = fun koSyncStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<KoSyncStatusPayload> =
future { future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
KoreaderSyncService.getStatus() KoreaderSyncService.getStatus()
} }
} }

View File

@@ -28,6 +28,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -42,13 +43,19 @@ import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.MangaStatus import suwayomi.tachidesk.manga.model.table.MangaStatus
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class MangaQuery { class MangaQuery {
fun manga( fun manga(
dataFetchingEnvironment: DataFetchingEnvironment, dataFetchingEnvironment: DataFetchingEnvironment,
id: Int, id: Int,
): CompletableFuture<MangaType> = dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id) ): CompletableFuture<MangaType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader("MangaDataLoader", id)
}
enum class MangaOrderBy( enum class MangaOrderBy(
override val column: Column<*>, override val column: Column<*>,
@@ -216,6 +223,7 @@ class MangaQuery {
} }
fun mangas( fun mangas(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: MangaCondition? = null, condition: MangaCondition? = null,
filter: MangaFilter? = null, filter: MangaFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -235,6 +243,7 @@ class MangaQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): MangaNodeList { ): MangaNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults = val queryResults =
transaction { transaction {
val res = val res =

View File

@@ -24,6 +24,7 @@ import suwayomi.tachidesk.graphql.queries.filter.OpAnd
import suwayomi.tachidesk.graphql.queries.filter.StringFilter import suwayomi.tachidesk.graphql.queries.filter.StringFilter
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -35,13 +36,19 @@ import suwayomi.tachidesk.graphql.server.primitives.lessNotUnique
import suwayomi.tachidesk.graphql.server.primitives.maybeSwap import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.GlobalMetaNodeList import suwayomi.tachidesk.graphql.types.GlobalMetaNodeList
import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class MetaQuery { class MetaQuery {
fun meta( fun meta(
dataFetchingEnvironment: DataFetchingEnvironment, dataFetchingEnvironment: DataFetchingEnvironment,
key: String, key: String,
): CompletableFuture<GlobalMetaType> = dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key) ): CompletableFuture<GlobalMetaType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader("GlobalMetaDataLoader", key)
}
enum class MetaOrderBy( enum class MetaOrderBy(
override val column: Column<*>, override val column: Column<*>,
@@ -105,6 +112,7 @@ class MetaQuery {
} }
fun metas( fun metas(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: MetaCondition? = null, condition: MetaCondition? = null,
filter: MetaFilter? = null, filter: MetaFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -124,6 +132,7 @@ class MetaQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): GlobalMetaNodeList { ): GlobalMetaNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val queryResults = val queryResults =
transaction { transaction {
val res = GlobalMetaTable.selectAll() val res = GlobalMetaTable.selectAll()

View File

@@ -1,7 +1,15 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import graphql.schema.DataFetchingEnvironment
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.SettingsType import suwayomi.tachidesk.graphql.types.SettingsType
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
class SettingsQuery { class SettingsQuery {
fun settings(): SettingsType = SettingsType() fun settings(dataFetchingEnvironment: DataFetchingEnvironment): SettingsType {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return SettingsType()
}
} }

View File

@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompare
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareEntity
import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString import suwayomi.tachidesk.graphql.queries.filter.andFilterWithCompareString
import suwayomi.tachidesk.graphql.queries.filter.applyOps 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.Cursor
import suwayomi.tachidesk.graphql.server.primitives.Order import suwayomi.tachidesk.graphql.server.primitives.Order
import suwayomi.tachidesk.graphql.server.primitives.OrderBy import suwayomi.tachidesk.graphql.server.primitives.OrderBy
@@ -39,13 +40,19 @@ import suwayomi.tachidesk.graphql.server.primitives.maybeSwap
import suwayomi.tachidesk.graphql.types.SourceNodeList import suwayomi.tachidesk.graphql.types.SourceNodeList
import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.graphql.types.SourceType
import suwayomi.tachidesk.manga.model.table.SourceTable 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 import java.util.concurrent.CompletableFuture
class SourceQuery { class SourceQuery {
fun source( fun source(
dataFetchingEnvironment: DataFetchingEnvironment, dataFetchingEnvironment: DataFetchingEnvironment,
id: Long, id: Long,
): CompletableFuture<SourceType> = dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id) ): CompletableFuture<SourceType> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", id)
}
enum class SourceOrderBy( enum class SourceOrderBy(
override val column: Column<*>, override val column: Column<*>,
@@ -121,6 +128,7 @@ class SourceQuery {
} }
fun sources( fun sources(
dataFetchingEnvironment: DataFetchingEnvironment,
condition: SourceCondition? = null, condition: SourceCondition? = null,
filter: SourceFilter? = null, filter: SourceFilter? = null,
@GraphQLDeprecated( @GraphQLDeprecated(
@@ -140,6 +148,7 @@ class SourceQuery {
last: Int? = null, last: Int? = null,
offset: Int? = null, offset: Int? = null,
): SourceNodeList { ): SourceNodeList {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (queryResults, resultsAsType) = val (queryResults, resultsAsType) =
transaction { transaction {
val res = SourceTable.selectAll() val res = SourceTable.selectAll()

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

View File

@@ -1,11 +1,16 @@
package suwayomi.tachidesk.graphql.queries package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
@@ -13,13 +18,24 @@ class UpdateQuery {
private val updater: IUpdater by injectLazy() private val updater: IUpdater by injectLazy()
@GraphQLDeprecated("Replaced with libraryUpdateStatus", ReplaceWith("libraryUpdateStatus")) @GraphQLDeprecated("Replaced with libraryUpdateStatus", ReplaceWith("libraryUpdateStatus"))
fun updateStatus(): CompletableFuture<UpdateStatus> = future { UpdateStatus(updater.status.first()) } fun updateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<UpdateStatus> =
future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
UpdateStatus(updater.status.first())
}
fun libraryUpdateStatus(): CompletableFuture<LibraryUpdateStatus> = future { LibraryUpdateStatus(updater.getStatus()) } fun libraryUpdateStatus(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<LibraryUpdateStatus> =
future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
LibraryUpdateStatus(updater.getStatus())
}
data class LastUpdateTimestampPayload( data class LastUpdateTimestampPayload(
val timestamp: Long, val timestamp: Long,
) )
fun lastUpdateTimestamp(): LastUpdateTimestampPayload = LastUpdateTimestampPayload(updater.getLastUpdateTimestamp()) fun lastUpdateTimestamp(dataFetchingEnvironment: DataFetchingEnvironment): LastUpdateTimestampPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return LastUpdateTimestampPayload(updater.getLastUpdateTimestamp())
}
} }

View File

@@ -9,34 +9,49 @@ package suwayomi.tachidesk.graphql.server
import com.expediagroup.graphql.server.execution.GraphQLContextFactory import com.expediagroup.graphql.server.execution.GraphQLContextFactory
import graphql.GraphQLContext import graphql.GraphQLContext
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.websocket.WsContext import io.javalin.websocket.WsContext
import org.dataloader.BatchLoaderEnvironment
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.UserType
/** /**
* Custom logic for how Suwayomi-Server should create its context given the [Context] * Custom logic for how Suwayomi-Server should create its context given the [Context]
*/ */
class TachideskGraphQLContextFactory : GraphQLContextFactory<Context> { class TachideskGraphQLContextFactory : GraphQLContextFactory<Context> {
override suspend fun generateContext(request: Context): GraphQLContext = emptyMap<Any, Any>().toGraphQLContext() override suspend fun generateContext(request: Context): GraphQLContext =
// mutableMapOf<Any, Any>( mapOf(
// "user" to User( Context::class to request,
// email = "fake@site.com", request.getPair(Attribute.TachideskUser),
// firstName = "Someone", ).toGraphQLContext()
// lastName = "You Don't know",
// universityId = 4
// )
// ).also { map ->
// request.headers["my-custom-header"]?.let { customHeader ->
// map["customHeader"] = customHeader
// }
// }.toGraphQLContext()
fun generateContextMap( fun generateContextMap(
@Suppress("UNUSED_PARAMETER") request: WsContext, user: UserType,
): Map<*, Any> = emptyMap<Any, Any>() request: WsContext,
): Map<*, Any> =
mapOf(
Context::class to request,
Attribute.TachideskUser to user,
)
private fun <T : Any> Context.getPair(attribute: Attribute<T>) = attribute to getAttribute(attribute)
private fun <T : Any> WsContext.getPair(attribute: Attribute<T>) = attribute to getAttribute(attribute)
} }
/** /**
* Create a [GraphQLContext] from [this] map * Create a [GraphQLContext] from [this] map
* @return a new [GraphQLContext] * @return a new [GraphQLContext]
*/ */
fun Map<*, Any?>.toGraphQLContext(): graphql.GraphQLContext = graphql.GraphQLContext.of(this) fun Map<*, Any?>.toGraphQLContext(): GraphQLContext = GraphQLContext.of(this)
fun <T : Any> GraphQLContext.getAttribute(attribute: Attribute<T>): T = get(attribute)
fun <T : Any> DataFetchingEnvironment.getAttribute(attribute: Attribute<T>): T = graphQlContext.get(attribute)
val BatchLoaderEnvironment.graphQlContext: GraphQLContext
get() = keyContextsList.filterIsInstance<GraphQLContext>().first()
fun <T : Any> BatchLoaderEnvironment.getAttribute(attribute: Attribute<T>): T = graphQlContext.getAttribute(attribute)

View File

@@ -27,6 +27,7 @@ import suwayomi.tachidesk.graphql.mutations.SettingsMutation
import suwayomi.tachidesk.graphql.mutations.SourceMutation import suwayomi.tachidesk.graphql.mutations.SourceMutation
import suwayomi.tachidesk.graphql.mutations.TrackMutation import suwayomi.tachidesk.graphql.mutations.TrackMutation
import suwayomi.tachidesk.graphql.mutations.UpdateMutation import suwayomi.tachidesk.graphql.mutations.UpdateMutation
import suwayomi.tachidesk.graphql.mutations.UserMutation
import suwayomi.tachidesk.graphql.queries.BackupQuery import suwayomi.tachidesk.graphql.queries.BackupQuery
import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.CategoryQuery
import suwayomi.tachidesk.graphql.queries.ChapterQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery
@@ -100,6 +101,7 @@ val schema =
TopLevelObject(SourceMutation()), TopLevelObject(SourceMutation()),
TopLevelObject(TrackMutation()), TopLevelObject(TrackMutation()),
TopLevelObject(UpdateMutation()), TopLevelObject(UpdateMutation()),
TopLevelObject(UserMutation()),
), ),
subscriptions = subscriptions =
listOf( listOf(

View File

@@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.convertValue
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.Header
import io.javalin.websocket.WsContext import io.javalin.websocket.WsContext
import io.javalin.websocket.WsMessageContext import io.javalin.websocket.WsMessageContext
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
@@ -36,6 +37,8 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_NEXT import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_NEXT
import suwayomi.tachidesk.graphql.server.toGraphQLContext import suwayomi.tachidesk.graphql.server.toGraphQLContext
import suwayomi.tachidesk.server.user.UserType
import suwayomi.tachidesk.server.user.getUserFromToken
/** /**
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina * Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
@@ -77,7 +80,7 @@ class ApolloSubscriptionProtocolHandler(
return try { return try {
when (operationMessage.type) { when (operationMessage.type) {
GQL_CONNECTION_INIT.type -> onInit(context) GQL_CONNECTION_INIT.type -> onInit(operationMessage, context)
GQL_SUBSCRIBE.type -> startSubscription(operationMessage, context) GQL_SUBSCRIBE.type -> startSubscription(operationMessage, context)
GQL_COMPLETE.type -> onComplete(operationMessage) GQL_COMPLETE.type -> onComplete(operationMessage)
GQL_PING.type -> onPing() GQL_PING.type -> onPing()
@@ -144,17 +147,27 @@ class ApolloSubscriptionProtocolHandler(
} }
} }
private fun onInit(context: WsContext): Flow<SubscriptionOperationMessage> { private fun onInit(
saveContext(context) operationMessage: SubscriptionOperationMessage,
context: WsContext,
): Flow<SubscriptionOperationMessage> {
@Suppress("UNCHECKED_CAST")
val payload = operationMessage.payload as? Map<String, Any?>
val token = payload?.let { it[Header.AUTHORIZATION] as? String }
saveContext(getUserFromToken(token), context)
return flowOf(acknowledgeMessage) return flowOf(acknowledgeMessage)
} }
/** /**
* Generate the context and save it for all future messages. * Generate the context and save it for all future messages.
*/ */
private fun saveContext(context: WsContext) { private fun saveContext(
user: UserType,
context: WsContext,
) {
runBlocking { runBlocking {
val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext() val graphQLContext = contextFactory.generateContextMap(user, context).toGraphQLContext()
sessionState.saveContext(context, graphQLContext) sessionState.saveContext(context, graphQLContext)
} }
} }

View File

@@ -9,18 +9,25 @@ package suwayomi.tachidesk.graphql.subscriptions
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.DownloadStatus import suwayomi.tachidesk.graphql.types.DownloadStatus
import suwayomi.tachidesk.graphql.types.DownloadUpdates import suwayomi.tachidesk.graphql.types.DownloadUpdates
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
class DownloadSubscription { class DownloadSubscription {
@GraphQLDeprecated("Replaced with downloadStatusChanged", ReplaceWith("downloadStatusChanged(input)")) @GraphQLDeprecated("Replaced with downloadStatusChanged", ReplaceWith("downloadStatusChanged(input)"))
fun downloadChanged(): Flow<DownloadStatus> = fun downloadChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow<DownloadStatus> {
DownloadManager.status.map { downloadStatus -> dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return DownloadManager.status.map { downloadStatus ->
DownloadStatus(downloadStatus) DownloadStatus(downloadStatus)
} }
}
data class DownloadChangedInput( data class DownloadChangedInput(
@GraphQLDescription( @GraphQLDescription(
@@ -33,7 +40,11 @@ class DownloadSubscription {
val maxUpdates: Int?, val maxUpdates: Int?,
) )
fun downloadStatusChanged(input: DownloadChangedInput): Flow<DownloadUpdates> { fun downloadStatusChanged(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DownloadChangedInput,
): Flow<DownloadUpdates> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val omitUpdates = input.maxUpdates != null val omitUpdates = input.maxUpdates != null
val maxUpdates = input.maxUpdates ?: 50 val maxUpdates = input.maxUpdates ?: 50

View File

@@ -1,9 +1,17 @@
package suwayomi.tachidesk.graphql.subscriptions package suwayomi.tachidesk.graphql.subscriptions
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
class InfoSubscription { class InfoSubscription {
fun webUIUpdateStatusChange(): Flow<WebUIUpdateStatus> = WebInterfaceManager.status fun webUIUpdateStatusChange(dataFetchingEnvironment: DataFetchingEnvironment): Flow<WebUIUpdateStatus> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return WebInterfaceManager.status
}
} }

View File

@@ -9,22 +9,29 @@ package suwayomi.tachidesk.graphql.subscriptions
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.UpdateStatus import suwayomi.tachidesk.graphql.types.UpdateStatus
import suwayomi.tachidesk.graphql.types.UpdaterUpdates import suwayomi.tachidesk.graphql.types.UpdaterUpdates
import suwayomi.tachidesk.manga.impl.update.IUpdater import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.manga.impl.update.UpdateUpdates import suwayomi.tachidesk.manga.impl.update.UpdateUpdates
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class UpdateSubscription { class UpdateSubscription {
private val updater: IUpdater by injectLazy() private val updater: IUpdater by injectLazy()
@GraphQLDeprecated("Replaced with updates", ReplaceWith("updates(input)")) @GraphQLDeprecated("Replaced with updates", ReplaceWith("updates(input)"))
fun updateStatusChanged(): Flow<UpdateStatus> = fun updateStatusChanged(dataFetchingEnvironment: DataFetchingEnvironment): Flow<UpdateStatus> {
updater.status.map { updateStatus -> dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
return updater.status.map { updateStatus ->
UpdateStatus(updateStatus) UpdateStatus(updateStatus)
} }
}
data class LibraryUpdateStatusChangedInput( data class LibraryUpdateStatusChangedInput(
@GraphQLDescription( @GraphQLDescription(
@@ -37,7 +44,11 @@ class UpdateSubscription {
val maxUpdates: Int?, val maxUpdates: Int?,
) )
fun libraryUpdateStatusChanged(input: LibraryUpdateStatusChangedInput): Flow<UpdaterUpdates> { fun libraryUpdateStatusChanged(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LibraryUpdateStatusChangedInput,
): Flow<UpdaterUpdates> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val omitUpdates = input.maxUpdates != null val omitUpdates = input.maxUpdates != null
val maxUpdates = input.maxUpdates ?: 50 val maxUpdates = input.maxUpdates ?: 50

View File

@@ -4,6 +4,7 @@ enum class AuthMode {
NONE, NONE,
BASIC_AUTH, BASIC_AUTH,
SIMPLE_LOGIN, SIMPLE_LOGIN,
UI_LOGIN,
// TODO: ACCOUNT for #623 // TODO: ACCOUNT for #623
; ;

View File

@@ -6,7 +6,10 @@ import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -28,6 +31,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
ProtoBackupImport.restoreLegacy(ctx.bodyInputStream()) ProtoBackupImport.restoreLegacy(ctx.bodyInputStream())
@@ -55,6 +59,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
// TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz"
ctx.future { ctx.future {
future { future {
@@ -80,6 +85,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
ctx.future { ctx.future {
future { future {
@@ -112,6 +118,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.contentType("application/octet-stream") ctx.contentType("application/octet-stream")
ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""") ctx.header("Content-Disposition", """attachment; filename="${Backup.getFilename()}"""")
@@ -146,6 +153,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
ProtoBackupValidator.validate(ctx.bodyInputStream()) ProtoBackupValidator.validate(ctx.bodyInputStream())
@@ -177,6 +185,7 @@ object BackupController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content()) ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content())

View File

@@ -12,6 +12,9 @@ import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass 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.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
@@ -28,6 +31,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Category.getCategoryList()) ctx.json(Category.getCategoryList())
}, },
withResults = { withResults = {
@@ -46,6 +50,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, name -> behaviorOf = { ctx, name ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
if (Category.createCategory(name) != -1) { if (Category.createCategory(name) != -1) {
ctx.status(200) ctx.status(200)
} else { } else {
@@ -73,6 +78,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate, includeInDownload -> behaviorOf = { ctx, categoryId, name, isDefault, includeInUpdate, includeInDownload ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Category.updateCategory(categoryId, name, isDefault, includeInUpdate, includeInDownload) Category.updateCategory(categoryId, name, isDefault, includeInUpdate, includeInDownload)
ctx.status(200) ctx.status(200)
}, },
@@ -92,6 +98,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, categoryId -> behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Category.removeCategory(categoryId) Category.removeCategory(categoryId)
ctx.status(200) ctx.status(200)
}, },
@@ -111,6 +118,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, categoryId -> behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(CategoryManga.getCategoryMangaList(categoryId)) ctx.json(CategoryManga.getCategoryMangaList(categoryId))
}, },
withResults = { withResults = {
@@ -130,6 +138,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, from, to -> behaviorOf = { ctx, from, to ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Category.reorderCategory(from, to) Category.reorderCategory(from, to)
ctx.status(200) ctx.status(200)
}, },
@@ -151,6 +160,7 @@ object CategoryController {
} }
}, },
behaviorOf = { ctx, categoryId, key, value -> behaviorOf = { ctx, categoryId, key, value ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Category.modifyMeta(categoryId, key, value) Category.modifyMeta(categoryId, key, value)
ctx.status(200) ctx.status(200)
}, },

View File

@@ -12,7 +12,10 @@ import io.javalin.websocket.WsConfig
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.manga.impl.download.DownloadManager
import suwayomi.tachidesk.manga.impl.download.DownloadManager.EnqueueInput 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.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -24,6 +27,7 @@ object DownloadController {
/** Download queue stats */ /** Download queue stats */
fun downloadsWS(ws: WsConfig) { fun downloadsWS(ws: WsConfig) {
ws.onConnect { ctx -> ws.onConnect { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.addClient(ctx) DownloadManager.addClient(ctx)
DownloadManager.notifyClient(ctx) DownloadManager.notifyClient(ctx)
} }
@@ -44,7 +48,8 @@ object DownloadController {
description("Start the downloader") description("Start the downloader")
} }
}, },
behaviorOf = { behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.start() DownloadManager.start()
}, },
withResults = { withResults = {
@@ -62,6 +67,7 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { DownloadManager.stop() } future { DownloadManager.stop() }
.thenApply { ctx.status(HttpStatus.OK) } .thenApply { ctx.status(HttpStatus.OK) }
@@ -82,6 +88,7 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { DownloadManager.clear() } future { DownloadManager.clear() }
.thenApply { ctx.status(HttpStatus.OK) } .thenApply { ctx.status(HttpStatus.OK) }
@@ -104,6 +111,7 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx, chapterIndex, mangaId -> behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex) DownloadManager.enqueueWithChapterIndex(mangaId, chapterIndex)
@@ -126,6 +134,7 @@ object DownloadController {
body<EnqueueInput>() body<EnqueueInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val inputs = json.decodeFromString<EnqueueInput>(ctx.body()) val inputs = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future { ctx.future {
future { future {
@@ -149,6 +158,7 @@ object DownloadController {
body<EnqueueInput>() body<EnqueueInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<EnqueueInput>(ctx.body()) val input = json.decodeFromString<EnqueueInput>(ctx.body())
ctx.future { ctx.future {
future { future {
@@ -173,6 +183,7 @@ object DownloadController {
} }
}, },
behaviorOf = { ctx, chapterIndex, mangaId -> behaviorOf = { ctx, chapterIndex, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
DownloadManager.dequeue(chapterIndex, mangaId) DownloadManager.dequeue(chapterIndex, mangaId)
ctx.status(200) ctx.status(200)
@@ -194,7 +205,8 @@ object DownloadController {
description("Reorder chapter in download queue") 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) DownloadManager.reorder(chapterIndex, mangaId, to)
}, },
withResults = { withResults = {

View File

@@ -12,7 +12,10 @@ import io.javalin.http.HttpStatus
import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.Extension
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList import suwayomi.tachidesk.manga.impl.extension.ExtensionsList
import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.withOperation import suwayomi.tachidesk.server.util.withOperation
@@ -31,6 +34,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
ExtensionsList.getExtensionList() ExtensionsList.getExtensionList()
@@ -55,6 +59,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, pkgName -> behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
Extension.installExtension(pkgName) Extension.installExtension(pkgName)
@@ -84,6 +89,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val uploadedFile = ctx.uploadedFile("file")!! val uploadedFile = ctx.uploadedFile("file")!!
logger.debug { "Uploaded extension file name: " + uploadedFile.filename() } logger.debug { "Uploaded extension file name: " + uploadedFile.filename() }
@@ -116,6 +122,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, pkgName -> behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
Extension.updateExtension(pkgName) Extension.updateExtension(pkgName)
@@ -143,6 +150,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, pkgName -> behaviorOf = { ctx, pkgName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Extension.uninstallExtension(pkgName) Extension.uninstallExtension(pkgName)
ctx.status(200) ctx.status(200)
}, },
@@ -165,6 +173,7 @@ object ExtensionController {
} }
}, },
behaviorOf = { ctx, apkName -> behaviorOf = { ctx, apkName ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Extension.getExtensionIcon(apkName) } future { Extension.getExtensionIcon(apkName) }
.thenApply { .thenApply {

View File

@@ -26,7 +26,10 @@ import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
@@ -49,6 +52,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
Manga.getManga(mangaId, onlineFetch) Manga.getManga(mangaId, onlineFetch)
@@ -73,6 +77,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
Manga.getMangaFull(mangaId, onlineFetch) Manga.getMangaFull(mangaId, onlineFetch)
@@ -96,6 +101,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Manga.getMangaThumbnail(mangaId) } future { Manga.getMangaThumbnail(mangaId) }
.thenApply { .thenApply {
@@ -123,6 +129,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Library.addMangaToLibrary(mangaId) } future { Library.addMangaToLibrary(mangaId) }
.thenApply { ctx.status(HttpStatus.OK) } .thenApply { ctx.status(HttpStatus.OK) }
@@ -145,6 +152,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Library.removeMangaFromLibrary(mangaId) } future { Library.removeMangaFromLibrary(mangaId) }
.thenApply { ctx.status(HttpStatus.OK) } .thenApply { ctx.status(HttpStatus.OK) }
@@ -167,6 +175,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(CategoryManga.getMangaCategories(mangaId)) ctx.json(CategoryManga.getMangaCategories(mangaId))
}, },
withResults = { withResults = {
@@ -186,6 +195,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, categoryId -> behaviorOf = { ctx, mangaId, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
CategoryManga.addMangaToCategory(mangaId, categoryId) CategoryManga.addMangaToCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
}, },
@@ -206,6 +216,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, categoryId -> behaviorOf = { ctx, mangaId, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
CategoryManga.removeMangaFromCategory(mangaId, categoryId) CategoryManga.removeMangaFromCategory(mangaId, categoryId)
ctx.status(200) ctx.status(200)
}, },
@@ -227,6 +238,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, key, value -> behaviorOf = { ctx, mangaId, key, value ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Manga.modifyMangaMeta(mangaId, key, value) Manga.modifyMangaMeta(mangaId, key, value)
ctx.status(200) ctx.status(200)
}, },
@@ -252,6 +264,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, onlineFetch -> behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Chapter.getChapterList(mangaId, onlineFetch) } future { Chapter.getChapterList(mangaId, onlineFetch) }
.thenApply { ctx.json(it) } .thenApply { ctx.json(it) }
@@ -275,6 +288,7 @@ object MangaController {
body<Chapter.MangaChapterBatchEditInput>() body<Chapter.MangaChapterBatchEditInput>()
}, },
behaviorOf = { ctx, mangaId -> behaviorOf = { ctx, mangaId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body()) val input = json.decodeFromString<Chapter.MangaChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters(input, mangaId) Chapter.modifyChapters(input, mangaId)
}, },
@@ -294,6 +308,7 @@ object MangaController {
body<Chapter.ChapterBatchEditInput>() body<Chapter.ChapterBatchEditInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body()) val input = json.decodeFromString<Chapter.ChapterBatchEditInput>(ctx.body())
Chapter.modifyChapters( Chapter.modifyChapters(
Chapter.MangaChapterBatchEditInput( Chapter.MangaChapterBatchEditInput(
@@ -320,6 +335,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex -> behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
var chapter = getChapterDownloadReadyByIndex(chapterIndex, mangaId) var chapter = getChapterDownloadReadyByIndex(chapterIndex, mangaId)
@@ -368,6 +384,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead -> behaviorOf = { ctx, mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val chapterId = Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) val chapterId = Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead)
// Sync with KoreaderSync when progress is updated // Sync with KoreaderSync when progress is updated
@@ -394,6 +411,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex -> behaviorOf = { ctx, mangaId, chapterIndex ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Chapter.deleteChapter(mangaId, chapterIndex) Chapter.deleteChapter(mangaId, chapterIndex)
ctx.status(200) ctx.status(200)
@@ -418,6 +436,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex, key, value -> behaviorOf = { ctx, mangaId, chapterIndex, key, value ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value)
ctx.status(200) ctx.status(200)
@@ -445,6 +464,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress, format -> behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress, format ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Page.getPageImage(mangaId, chapterIndex, index, format, null) } future { Page.getPageImage(mangaId, chapterIndex, index, format, null) }
.thenApply { .thenApply {
@@ -480,6 +500,7 @@ object MangaController {
} }
}, },
behaviorOf = { ctx, chapterId, markAsRead -> behaviorOf = { ctx, chapterId, markAsRead ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
if (ctx.method() == HandlerType.HEAD) { if (ctx.method() == HandlerType.HEAD) {
ctx.future { ctx.future {
future { ChapterDownloadHelper.getCbzMetadataForDownload(chapterId) } future { ChapterDownloadHelper.getCbzMetadataForDownload(chapterId) }

View File

@@ -17,7 +17,10 @@ import suwayomi.tachidesk.manga.impl.Source
import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange
import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass
import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
@@ -35,6 +38,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Source.getSourceList()) ctx.json(Source.getSourceList())
}, },
withResults = { withResults = {
@@ -53,6 +57,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Source.getSource(sourceId)!!) ctx.json(Source.getSource(sourceId)!!)
}, },
withResults = { withResults = {
@@ -73,6 +78,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = true) MangaList.getMangaList(sourceId, pageNum, popular = true)
@@ -96,6 +102,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
MangaList.getMangaList(sourceId, pageNum, popular = false) MangaList.getMangaList(sourceId, pageNum, popular = false)
@@ -118,6 +125,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Source.getSourcePreferences(sourceId)) ctx.json(Source.getSourcePreferences(sourceId))
}, },
withResults = { withResults = {
@@ -137,6 +145,7 @@ object SourceController {
body<SourcePreferenceChange>() body<SourcePreferenceChange>()
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java)
ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value)) ctx.json(Source.setSourcePreference(sourceId, preferenceChange.position, preferenceChange.value))
}, },
@@ -157,6 +166,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, reset -> behaviorOf = { ctx, sourceId, reset ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Search.getFilterList(sourceId, reset)) ctx.json(Search.getFilterList(sourceId, reset))
}, },
withResults = { withResults = {
@@ -179,6 +189,7 @@ object SourceController {
body<Array<FilterChange>>() body<Array<FilterChange>>()
}, },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val filterChange = val filterChange =
try { try {
json.decodeFromString<List<FilterChange>>(ctx.body()) json.decodeFromString<List<FilterChange>>(ctx.body())
@@ -206,6 +217,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, sourceId, searchTerm, pageNum -> behaviorOf = { ctx, sourceId, searchTerm, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Search.sourceSearch(sourceId, searchTerm, pageNum) } future { Search.sourceSearch(sourceId, searchTerm, pageNum) }
.thenApply { ctx.json(it) } .thenApply { ctx.json(it) }
@@ -229,6 +241,7 @@ object SourceController {
body<FilterData>() body<FilterData>()
}, },
behaviorOf = { ctx, sourceId, pageNum -> behaviorOf = { ctx, sourceId, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val filter = json.decodeFromString<FilterData>(ctx.body()) val filter = json.decodeFromString<FilterData>(ctx.body())
ctx.future { ctx.future {
future { Search.sourceFilter(sourceId, pageNum, filter) } future { Search.sourceFilter(sourceId, pageNum, filter) }
@@ -251,6 +264,7 @@ object SourceController {
} }
}, },
behaviorOf = { ctx, searchTerm -> behaviorOf = { ctx, searchTerm ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
// TODO // TODO
ctx.json(Search.sourceGlobalSearch(searchTerm)) ctx.json(Search.sourceGlobalSearch(searchTerm))
}, },

View File

@@ -12,7 +12,10 @@ import io.javalin.http.HttpStatus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import suwayomi.tachidesk.manga.impl.track.Track import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
@@ -33,6 +36,7 @@ object TrackController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.json(Track.getTrackerList()) ctx.json(Track.getTrackerList())
}, },
withResults = { withResults = {
@@ -50,6 +54,7 @@ object TrackController {
body<Track.LoginInput>() body<Track.LoginInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Track.LoginInput>(ctx.body()) val input = json.decodeFromString<Track.LoginInput>(ctx.body())
logger.debug { "tracker login $input" } logger.debug { "tracker login $input" }
ctx.future { ctx.future {
@@ -73,6 +78,7 @@ object TrackController {
body<Track.LogoutInput>() body<Track.LogoutInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Track.LogoutInput>(ctx.body()) val input = json.decodeFromString<Track.LogoutInput>(ctx.body())
logger.debug { "tracker logout $input" } logger.debug { "tracker logout $input" }
ctx.future { ctx.future {
@@ -96,6 +102,7 @@ object TrackController {
body<Track.SearchInput>() body<Track.SearchInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Track.SearchInput>(ctx.body()) val input = json.decodeFromString<Track.SearchInput>(ctx.body())
logger.debug { "tracker search $input" } logger.debug { "tracker search $input" }
ctx.future { ctx.future {
@@ -122,6 +129,7 @@ object TrackController {
} }
}, },
behaviorOf = { ctx, mangaId, trackerId, remoteId, private -> behaviorOf = { ctx, mangaId, trackerId, remoteId, private ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Track.bind(mangaId, trackerId, remoteId.toLong(), private) } future { Track.bind(mangaId, trackerId, remoteId.toLong(), private) }
.thenApply { ctx.status(HttpStatus.OK) } .thenApply { ctx.status(HttpStatus.OK) }
@@ -142,6 +150,7 @@ object TrackController {
body<Track.UpdateInput>() body<Track.UpdateInput>()
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val input = json.decodeFromString<Track.UpdateInput>(ctx.body()) val input = json.decodeFromString<Track.UpdateInput>(ctx.body())
logger.debug { "tracker update $input" } logger.debug { "tracker update $input" }
ctx.future { ctx.future {
@@ -164,6 +173,7 @@ object TrackController {
} }
}, },
behaviorOf = { ctx, trackerId -> behaviorOf = { ctx, trackerId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { Track.getTrackerThumbnail(trackerId) } future { Track.getTrackerThumbnail(trackerId) }
.thenApply { .thenApply {

View File

@@ -10,7 +10,10 @@ import suwayomi.tachidesk.manga.impl.update.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.formParam
import suwayomi.tachidesk.server.util.handler import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
@@ -39,6 +42,7 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx, pageNum -> behaviorOf = { ctx, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future { ctx.future {
future { future {
Chapter.getRecentChapters(pageNum) Chapter.getRecentChapters(pageNum)
@@ -66,6 +70,7 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx, categoryId -> behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>() val updater = Injekt.get<IUpdater>()
if (categoryId == null) { if (categoryId == null) {
logger.info { "Adding Library to Update Queue" } logger.info { "Adding Library to Update Queue" }
@@ -96,6 +101,7 @@ object UpdateController {
fun categoryUpdateWS(ws: WsConfig) { fun categoryUpdateWS(ws: WsConfig) {
ws.onConnect { ctx -> ws.onConnect { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
UpdaterSocket.addClient(ctx) UpdaterSocket.addClient(ctx)
} }
ws.onMessage { ctx -> ws.onMessage { ctx ->
@@ -115,6 +121,7 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>() val updater = Injekt.get<IUpdater>()
ctx.json(updater.statusDeprecated.value) ctx.json(updater.statusDeprecated.value)
}, },
@@ -132,6 +139,7 @@ object UpdateController {
} }
}, },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>() val updater = Injekt.get<IUpdater>()
logger.info { "Resetting Updater" } logger.info { "Resetting Updater" }
ctx.future { ctx.future {

View File

@@ -9,7 +9,10 @@ import suwayomi.tachidesk.opds.dto.OpdsMangaFilter
import suwayomi.tachidesk.opds.dto.OpdsSearchCriteria import suwayomi.tachidesk.opds.dto.OpdsSearchCriteria
import suwayomi.tachidesk.opds.dto.PrimaryFilterType import suwayomi.tachidesk.opds.dto.PrimaryFilterType
import suwayomi.tachidesk.opds.impl.OpdsFeedBuilder import suwayomi.tachidesk.opds.impl.OpdsFeedBuilder
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future 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.handler
import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam import suwayomi.tachidesk.server.util.queryParam
@@ -62,6 +65,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, lang -> behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.contentType(OPDS_MIME).result(OpdsFeedBuilder.getRootFeed(BASE_URL, locale)) ctx.contentType(OPDS_MIME).result(OpdsFeedBuilder.getRootFeed(BASE_URL, locale))
}, },
@@ -84,6 +88,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -109,6 +114,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, lang -> behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.contentType("application/opensearchdescription+xml").result( ctx.contentType("application/opensearchdescription+xml").result(
""" """
@@ -136,6 +142,7 @@ object OpdsV1Controller {
handler( handler(
documentWith = { withOperation { summary("OPDS Series in Library Feed") } }, documentWith = { withOperation { summary("OPDS Series in Library Feed") } },
behaviorOf = { ctx -> behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val pageNumber = ctx.queryParam("pageNumber")?.toIntOrNull() val pageNumber = ctx.queryParam("pageNumber")?.toIntOrNull()
val query = ctx.queryParam("query") val query = ctx.queryParam("query")
val author = ctx.queryParam("author") val author = ctx.queryParam("author")
@@ -188,6 +195,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -214,6 +222,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -240,6 +249,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -266,6 +276,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -291,6 +302,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, lang -> behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -316,6 +328,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, lang -> behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -342,6 +355,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, pageNumber, lang -> behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -370,6 +384,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, sourceId, pageNumber, sort, lang -> behaviorOf = { ctx, sourceId, pageNumber, sort, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -405,6 +420,7 @@ object OpdsV1Controller {
pathParam<Long>("sourceId"), pathParam<Long>("sourceId"),
documentWith = { withOperation { summary("OPDS Library Source Specific Series Feed") } }, documentWith = { withOperation { summary("OPDS Library Source Specific Series Feed") } },
behaviorOf = { ctx, sourceId -> behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(sourceId = sourceId, primaryFilter = PrimaryFilterType.SOURCE)) val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(sourceId = sourceId, primaryFilter = PrimaryFilterType.SOURCE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
}, },
@@ -422,6 +438,7 @@ object OpdsV1Controller {
pathParam<Int>("categoryId"), pathParam<Int>("categoryId"),
documentWith = { withOperation { summary("OPDS Category Specific Series Feed") } }, documentWith = { withOperation { summary("OPDS Category Specific Series Feed") } },
behaviorOf = { ctx, categoryId -> behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = val criteria =
buildCriteriaFromContext(ctx, OpdsMangaFilter(categoryId = categoryId, primaryFilter = PrimaryFilterType.CATEGORY)) buildCriteriaFromContext(ctx, OpdsMangaFilter(categoryId = categoryId, primaryFilter = PrimaryFilterType.CATEGORY))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
@@ -440,6 +457,7 @@ object OpdsV1Controller {
pathParam<String>("genre"), pathParam<String>("genre"),
documentWith = { withOperation { summary("OPDS Genre Specific Series Feed") } }, documentWith = { withOperation { summary("OPDS Genre Specific Series Feed") } },
behaviorOf = { ctx, genre -> behaviorOf = { ctx, genre ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(genre = genre, primaryFilter = PrimaryFilterType.GENRE)) val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(genre = genre, primaryFilter = PrimaryFilterType.GENRE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
}, },
@@ -457,6 +475,7 @@ object OpdsV1Controller {
pathParam<Int>("statusId"), pathParam<Int>("statusId"),
documentWith = { withOperation { summary("OPDS Status Specific Series Feed") } }, documentWith = { withOperation { summary("OPDS Status Specific Series Feed") } },
behaviorOf = { ctx, statusId -> behaviorOf = { ctx, statusId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(statusId = statusId, primaryFilter = PrimaryFilterType.STATUS)) val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(statusId = statusId, primaryFilter = PrimaryFilterType.STATUS))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
}, },
@@ -479,6 +498,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, langCode -> behaviorOf = { ctx, langCode ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = val criteria =
buildCriteriaFromContext(ctx, OpdsMangaFilter(langCode = langCode, primaryFilter = PrimaryFilterType.LANGUAGE)) buildCriteriaFromContext(ctx, OpdsMangaFilter(langCode = langCode, primaryFilter = PrimaryFilterType.LANGUAGE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria) getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
@@ -506,6 +526,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, seriesId, pageNumber, sort, filter, lang -> behaviorOf = { ctx, seriesId, pageNumber, sort, filter, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {
@@ -536,6 +557,7 @@ object OpdsV1Controller {
} }
}, },
behaviorOf = { ctx, seriesId, chapterIndex, lang -> behaviorOf = { ctx, seriesId, chapterIndex, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang) val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future { ctx.future {
future { future {

View File

@@ -12,12 +12,14 @@ import gg.jte.TemplateEngine
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.http.Context
import io.javalin.http.HandlerType import io.javalin.http.HandlerType
import io.javalin.http.HttpStatus import io.javalin.http.HttpStatus
import io.javalin.http.RedirectResponse import io.javalin.http.RedirectResponse
import io.javalin.http.UnauthorizedResponse import io.javalin.http.UnauthorizedResponse
import io.javalin.http.staticfiles.Location import io.javalin.http.staticfiles.Location
import io.javalin.rendering.template.JavalinJte import io.javalin.rendering.template.JavalinJte
import io.javalin.websocket.WsContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -31,6 +33,11 @@ import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.i18n.LocalizationHelper import suwayomi.tachidesk.i18n.LocalizationHelper
import suwayomi.tachidesk.manga.MangaAPI import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.opds.OpdsAPI import suwayomi.tachidesk.opds.OpdsAPI
import suwayomi.tachidesk.server.user.ForbiddenException
import suwayomi.tachidesk.server.user.UnauthorizedException
import suwayomi.tachidesk.server.user.UserType
import suwayomi.tachidesk.server.user.getUserFromContext
import suwayomi.tachidesk.server.user.getUserFromWsContext
import suwayomi.tachidesk.server.util.Browser import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.WebInterfaceManager import suwayomi.tachidesk.server.util.WebInterfaceManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -207,6 +214,8 @@ object JavalinSetup {
ctx.header("WWW-Authenticate", "Basic") ctx.header("WWW-Authenticate", "Basic")
throw UnauthorizedResponse() throw UnauthorizedResponse()
} }
ctx.setAttribute(Attribute.TachideskUser, getUserFromContext(ctx))
} }
app.events { event -> app.events { event ->
@@ -217,6 +226,12 @@ object JavalinSetup {
} }
} }
app.wsBefore {
it.onConnect { ctx ->
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
}
}
// when JVM is prompted to shutdown, stop javalin gracefully // when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook( Runtime.getRuntime().addShutdownHook(
thread(start = false) { thread(start = false) {
@@ -244,6 +259,18 @@ object JavalinSetup {
ctx.result(e.message ?: "Bad Request") ctx.result(e.message ?: "Bad Request")
} }
app.exception(UnauthorizedException::class.java) { e, ctx ->
logger.error(e) { "UnauthorizedException while handling the request" }
ctx.status(HttpStatus.UNAUTHORIZED)
ctx.result(e.message ?: "Unauthorized")
}
app.exception(ForbiddenException::class.java) { e, ctx ->
logger.error(e) { "ForbiddenException while handling the request" }
ctx.status(HttpStatus.FORBIDDEN)
ctx.result(e.message ?: "Forbidden")
}
app.start() app.start()
} }
@@ -262,4 +289,28 @@ object JavalinSetup {
// ) // )
// } // }
// } // }
sealed class Attribute<T : Any>(
val name: String,
) {
data object TachideskUser : Attribute<UserType>("user")
}
private fun <T : Any> Context.setAttribute(
attribute: Attribute<T>,
value: T,
) {
attribute(attribute.name, value)
}
private fun <T : Any> WsContext.setAttribute(
attribute: Attribute<T>,
value: T,
) {
attribute(attribute.name, value)
}
fun <T : Any> Context.getAttribute(attribute: Attribute<T>): T = attribute(attribute.name)!!
fun <T : Any> WsContext.getAttribute(attribute: Attribute<T>): T = attribute(attribute.name)!!
} }

View File

@@ -34,6 +34,7 @@ import suwayomi.tachidesk.graphql.types.WebUIInterface
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
import kotlin.time.Duration
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -152,6 +153,9 @@ class ServerConfig(
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue() val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
val authUsername: MutableStateFlow<String> by OverrideConfigValue() val authUsername: MutableStateFlow<String> by OverrideConfigValue()
val authPassword: MutableStateFlow<String> by OverrideConfigValue() val authPassword: MutableStateFlow<String> by OverrideConfigValue()
val jwtAudience: MutableStateFlow<String> by OverrideConfigValue()
val jwtTokenExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
val jwtRefreshExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({ val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
authMode.value == AuthMode.BASIC_AUTH authMode.value == AuthMode.BASIC_AUTH
}) { }) {

View File

@@ -48,6 +48,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.renameTo
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.DurationType
import suwayomi.tachidesk.server.util.MutableStateFlowType import suwayomi.tachidesk.server.util.MutableStateFlowType
import suwayomi.tachidesk.server.util.SystemTray import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@@ -176,6 +177,7 @@ fun applicationSetup() {
// register Tachidesk's config which is dubbed "ServerConfig" // register Tachidesk's config which is dubbed "ServerConfig"
registerCustomType(MutableStateFlowType()) registerCustomType(MutableStateFlowType())
registerCustomType(DurationType())
GlobalConfigManager.registerModule( GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config }, ServerConfig.register { GlobalConfigManager.config },
) )

View File

@@ -0,0 +1,53 @@
package suwayomi.tachidesk.server.user
import io.javalin.http.Context
import io.javalin.http.Header
import io.javalin.websocket.WsConnectContext
import suwayomi.tachidesk.global.impl.util.Jwt
import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.server.serverConfig
sealed class UserType {
class Admin(
val id: Int,
) : UserType()
data object Visitor : UserType()
}
fun UserType.requireUser(): Int =
when (this) {
is UserType.Admin -> id
UserType.Visitor -> throw UnauthorizedException()
}
fun getUserFromToken(token: String?): UserType {
if (serverConfig.authMode.value != AuthMode.UI_LOGIN) {
return UserType.Admin(1)
}
if (token.isNullOrBlank()) {
return UserType.Visitor
}
return Jwt.verifyJwt(token)
}
fun getUserFromContext(ctx: Context): UserType {
val authentication = ctx.header(Header.AUTHORIZATION) ?: ctx.cookie("suwayomi-server-token")
val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token")
return getUserFromToken(token)
}
fun getUserFromWsContext(ctx: WsConnectContext): UserType {
val authentication =
ctx.header(Header.AUTHORIZATION) ?: ctx.header("Sec-WebSocket-Protocol") ?: ctx.cookie("suwayomi-server-token")
val token = authentication?.substringAfter("Bearer ") ?: ctx.queryParam("token")
return getUserFromToken(token)
}
class UnauthorizedException : IllegalStateException("Unauthorized")
class ForbiddenException : IllegalStateException("Forbidden")

View File

@@ -0,0 +1,31 @@
package suwayomi.tachidesk.server.util
import com.typesafe.config.Config
import io.github.config4k.ClassContainer
import io.github.config4k.CustomType
import io.github.config4k.readers.SelectReader
import io.github.config4k.toConfig
import kotlin.time.Duration
class DurationType : CustomType {
override fun parse(
clazz: ClassContainer,
config: Config,
name: String,
): Any? {
val clazz = ClassContainer(String::class)
val reader = SelectReader.getReader(clazz)
val path = name
val result = reader(config, path) as String
return Duration.parse(result)
}
override fun testParse(clazz: ClassContainer): Boolean = clazz.mapperClass.qualifiedName == "kotlin.time.Duration"
override fun testToConfig(obj: Any): Boolean = obj as? Duration != null
override fun toConfig(
obj: Any,
name: String,
): Config = (obj as Duration).toString().toConfig(name)
}

View File

@@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
# Authentication # Authentication
server.authMode = "none" # none, basic_auth or simple_login server.authMode = "none" # none, basic_auth, simple_login or ui_login
server.authUsername = "" server.authUsername = ""
server.authPassword = "" server.authPassword = ""
server.jwtAudience = "suwayomi-server-api"
server.jwtTokenExpiry = "5m"
server.jwtRefreshExpiry = "60d"
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false

View File

@@ -49,9 +49,12 @@ server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't ha
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
# Authentication # Authentication
server.authMode = "none" # none, basic_auth or simple_login server.authMode = "none" # none, basic_auth, simple_login or ui_login
server.authUsername = "" server.authUsername = ""
server.authPassword = "" server.authPassword = ""
server.jwtAudience = "suwayomi-server-api"
server.jwtTokenExpiry = "5m"
server.jwtRefreshExpiry = "60d"
# misc # misc
server.debugLogsEnabled = false server.debugLogsEnabled = false