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
kcef = "dev.datlag:kcef:2024.04.20.4"
# User
jwt = "com.auth0:java-jwt:4.4.0"
# lint - used for renovate to update ktlint version
ktlint = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "ktlint" }

View File

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

View File

@@ -142,6 +142,7 @@
<string name="webview_label_loading">Loading page...</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_login_required">Your configuration requires you to login. Please enter username and password.</string>
<string name="webview_placeholder_url">Enter URL...</string>
<string name="login_label_title">Suwayomi Login</string>

View File

@@ -159,25 +159,26 @@
main .contextmenu button:hover {
background: #eee;
}
.copydialog {
.copydialog, .logindialog {
display: none;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
padding: 6px;
z-index: 1;
}
.copydialog.show {
.copydialog.show, .logindialog.show {
display: block;
}
.copydialog::before {
.copydialog::before, .logindialog::before {
content: '';
position: absolute;
inset: 0;
background: black;
opacity: 0.3;
}
.copydialog__inner {
.copydialog__inner, .logindialog__inner {
position: relative;
max-width: 960px;
border-radius: 8px;
@@ -204,10 +205,10 @@
line-height: 1;
}
@media (min-width: 500px) {
.copydialog {
.copydialog, .logindialog {
padding: 24px;
}
.copydialog__inner {
.copydialog__inner, .logindialog__inner {
padding: 12px 18px;
height: auto;
}
@@ -222,6 +223,86 @@
border-bottom: 9px solid transparent;
border-left: 9px solid currentcolor;
}
.logindialog .error {
margin: 8px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid #b71c1c;
background-color: #c62828;
color: white;
}
.logindialog .error:empty {
display: none;
}
.logindialog form label {
cursor: pointer;
}
.logindialog form button {
all: unset;
padding: 8px;
line-height: 1.75;
text-align: center;
min-width: 64px;
border-radius: 4px;
padding: 6px 8px;
color: rgb(91, 116, 239);
text-transform: uppercase;
letter-spacing: 0.02857em;
}
.logindialog form button:not([disabled]) {
cursor: pointer;
}
.logindialog form button:not([disabled]):hover {
background-color: rgba(91, 116, 239, 0.08);
}
.logindialog form input {
all: unset;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.23);
padding: 6px 12px;
width: auto;
min-width: 0;
}
.logindialog form input:hover {
border-color: white;
}
.logindialog form input:focus {
border-color: rgb(91, 116, 239);
}
.logindialog form .controls {
display: grid;
align-items: center;
grid-template-columns: 1fr;
}
.logindialog form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 6px;
}
.logindialog form .submit {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 24px;
}
.logindialog input:disabled, .logindialog button:disabled {
opacity: 0.7;
}
@media (min-width: 500px) {
.logindialog form {
width: 100%;
max-width: 450px;
margin: 8px auto;
}
.logindialog form .controls {
grid-template-columns: auto 1fr;
column-gap: 16px;
row-gap: 6px;
}
.logindialog form .controls > :nth-child(even):not(:last-child) {
margin-bottom: 0px;
}
}
</style>
</head>
<body>
@@ -256,6 +337,24 @@
<input type="text" id="copyinput" disabled readonly/>
</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>
const messageDiv = document.getElementById('message');
const statusDiv = document.getElementById('status');
@@ -274,10 +373,89 @@
const titleDiv = document.getElementById('title');
const reverseToggle = document.getElementById('reverseScroll');
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 {
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;
goButton.disabled = false;
@@ -393,11 +571,6 @@
/// Server events
socket.addEventListener('open', () => {
loadUrl(url);
console.log('WebSocket connection opened');
});
socket.addEventListener('message', e => {
const obj = JSON.parse(e.data);
switch (obj.type) {
@@ -449,7 +622,7 @@
if (e.wasClean) {
console.log(`WebSocket connection closed cleanly, code=` + e.code + `, reason=` + e.reason);
} else {
console.error('WebSocket connection died');
console.error('WebSocket connection died', e);
}
document.body.classList.add('disconnected');
});
@@ -583,11 +756,13 @@
};
attachEvents();
frameInput.focus();
loadUrl(url);
} 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');
console.error(e);
}
})();
</script>
</body>
</html>

View File

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

View File

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

View File

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

View File

@@ -89,6 +89,10 @@ object WebView : Websocket<String>() {
@SerialName("copy")
class JsCopyMessage : TypeObject()
@Serializable
@SerialName("ping")
class JsPingMessage : TypeObject()
override fun handleRequest(ctx: WsMessageContext) {
val dr = driver ?: return
try {
@@ -113,6 +117,9 @@ object WebView : Websocket<String>() {
is JsCopyMessage -> {
dr.copy()
}
is JsPingMessage -> {
notifyAllClients("{\"type\":\"pong\"}")
}
}
} catch (e: Exception) {
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
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.UploadedFile
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.server.TemporaryFileStorage
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.BackupRestoreStatus
import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
import kotlin.time.Duration.Companion.seconds
@@ -26,7 +31,11 @@ class BackupMutation {
val status: BackupRestoreStatus?,
)
fun restoreBackup(input: RestoreBackupInput): CompletableFuture<RestoreBackupPayload> {
fun restoreBackup(
dataFetchingEnvironment: DataFetchingEnvironment,
input: RestoreBackupInput,
): CompletableFuture<RestoreBackupPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, backup) = input
return future {
@@ -53,7 +62,11 @@ class BackupMutation {
val url: String,
)
fun createBackup(input: CreateBackupInput? = null): CreateBackupPayload {
fun createBackup(
dataFetchingEnvironment: DataFetchingEnvironment,
input: CreateBackupInput? = null,
): CreateBackupPayload {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val filename = Backup.getFilename()
val backup =

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,12 +6,14 @@ import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.SwitchPreferenceCompat
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.FilterChange
import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.graphql.types.Preference
@@ -25,7 +27,10 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.SourceMetaTable
import suwayomi.tachidesk.manga.model.table.SourceTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
class SourceMutation {
@@ -39,7 +44,11 @@ class SourceMutation {
val meta: SourceMetaType,
)
fun setSourceMeta(input: SetSourceMetaInput): DataFetcherResult<SetSourceMetaPayload?> {
fun setSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: SetSourceMetaInput,
): DataFetcherResult<SetSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, meta) = input
return asDataFetcherResult {
@@ -61,7 +70,11 @@ class SourceMutation {
val source: SourceType?,
)
fun deleteSourceMeta(input: DeleteSourceMetaInput): DataFetcherResult<DeleteSourceMetaPayload?> {
fun deleteSourceMeta(
dataFetchingEnvironment: DataFetchingEnvironment,
input: DeleteSourceMetaInput,
): DataFetcherResult<DeleteSourceMetaPayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, key) = input
return asDataFetcherResult {
@@ -116,7 +129,11 @@ class SourceMutation {
val hasNextPage: Boolean,
)
fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
fun fetchSourceManga(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchSourceMangaInput,
): CompletableFuture<DataFetcherResult<FetchSourceMangaPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, type, page, query, filters) = input
return future {
@@ -182,7 +199,11 @@ class SourceMutation {
val source: SourceType,
)
fun updateSourcePreference(input: UpdateSourcePreferenceInput): DataFetcherResult<UpdateSourcePreferencePayload?> {
fun updateSourcePreference(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateSourcePreferenceInput,
): DataFetcherResult<UpdateSourcePreferencePayload?> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, sourceId, change) = input
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.GraphQLDescription
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.graphql.asDataFetcherResult
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.TrackRecordType
import suwayomi.tachidesk.graphql.types.TrackerType
import suwayomi.tachidesk.manga.impl.track.Track
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import java.util.concurrent.CompletableFuture
class TrackMutation {
@@ -28,7 +33,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun loginTrackerOAuth(input: LoginTrackerOAuthInput): CompletableFuture<LoginTrackerOAuthPayload> {
fun loginTrackerOAuth(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LoginTrackerOAuthInput,
): CompletableFuture<LoginTrackerOAuthPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -57,7 +66,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun loginTrackerCredentials(input: LoginTrackerCredentialsInput): CompletableFuture<LoginTrackerCredentialsPayload> {
fun loginTrackerCredentials(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LoginTrackerCredentialsInput,
): CompletableFuture<LoginTrackerCredentialsPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -84,7 +97,11 @@ class TrackMutation {
val tracker: TrackerType,
)
fun logoutTracker(input: LogoutTrackerInput): CompletableFuture<LogoutTrackerPayload> {
fun logoutTracker(
dataFetchingEnvironment: DataFetchingEnvironment,
input: LogoutTrackerInput,
): CompletableFuture<LogoutTrackerPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val tracker =
requireNotNull(TrackerManager.getTracker(input.trackerId)) {
"Could not find tracker"
@@ -117,7 +134,11 @@ class TrackMutation {
val trackRecord: TrackRecordType,
)
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
fun bindTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: BindTrackInput,
): CompletableFuture<BindTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId, trackerId, remoteId, private) = input
return future {
@@ -152,7 +173,11 @@ class TrackMutation {
val trackRecord: TrackRecordType,
)
fun fetchTrack(input: FetchTrackInput): CompletableFuture<FetchTrackPayload> {
fun fetchTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: FetchTrackInput,
): CompletableFuture<FetchTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, recordId) = input
return future {
@@ -184,7 +209,11 @@ class TrackMutation {
val trackRecord: TrackRecordType?,
)
fun unbindTrack(input: UnbindTrackInput): CompletableFuture<UnbindTrackPayload> {
fun unbindTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UnbindTrackInput,
): CompletableFuture<UnbindTrackPayload> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, recordId, deleteRemoteTrack) = input
return future {
@@ -214,7 +243,11 @@ class TrackMutation {
val trackRecords: List<TrackRecordType>,
)
fun trackProgress(input: TrackProgressInput): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
fun trackProgress(
dataFetchingEnvironment: DataFetchingEnvironment,
input: TrackProgressInput,
): CompletableFuture<DataFetcherResult<TrackProgressPayload?>> {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, mangaId) = input
return future {
@@ -256,8 +289,12 @@ class TrackMutation {
val trackRecord: TrackRecordType?,
)
fun updateTrack(input: UpdateTrackInput): CompletableFuture<UpdateTrackPayload> =
fun updateTrack(
dataFetchingEnvironment: DataFetchingEnvironment,
input: UpdateTrackInput,
): CompletableFuture<UpdateTrackPayload> =
future {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
Track.update(
Track.UpdateInput(
input.recordId,

View File

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

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

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

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

@@ -1,11 +1,16 @@
package suwayomi.tachidesk.graphql.queries
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.first
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.LibraryUpdateStatus
import suwayomi.tachidesk.graphql.types.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.IUpdater
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.CompletableFuture
@@ -13,13 +18,24 @@ class UpdateQuery {
private val updater: IUpdater by injectLazy()
@GraphQLDeprecated("Replaced with libraryUpdateStatus", ReplaceWith("libraryUpdateStatus"))
fun updateStatus(): CompletableFuture<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(
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 graphql.GraphQLContext
import graphql.schema.DataFetchingEnvironment
import io.javalin.http.Context
import io.javalin.websocket.WsContext
import org.dataloader.BatchLoaderEnvironment
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.UserType
/**
* Custom logic for how Suwayomi-Server should create its context given the [Context]
*/
class TachideskGraphQLContextFactory : GraphQLContextFactory<Context> {
override suspend fun generateContext(request: Context): GraphQLContext = emptyMap<Any, Any>().toGraphQLContext()
// mutableMapOf<Any, Any>(
// "user" to User(
// email = "fake@site.com",
// firstName = "Someone",
// lastName = "You Don't know",
// universityId = 4
// )
// ).also { map ->
// request.headers["my-custom-header"]?.let { customHeader ->
// map["customHeader"] = customHeader
// }
// }.toGraphQLContext()
override suspend fun generateContext(request: Context): GraphQLContext =
mapOf(
Context::class to request,
request.getPair(Attribute.TachideskUser),
).toGraphQLContext()
fun generateContextMap(
@Suppress("UNUSED_PARAMETER") request: WsContext,
): Map<*, Any> = emptyMap<Any, Any>()
user: UserType,
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
* @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.TrackMutation
import suwayomi.tachidesk.graphql.mutations.UpdateMutation
import suwayomi.tachidesk.graphql.mutations.UserMutation
import suwayomi.tachidesk.graphql.queries.BackupQuery
import suwayomi.tachidesk.graphql.queries.CategoryQuery
import suwayomi.tachidesk.graphql.queries.ChapterQuery
@@ -100,6 +101,7 @@ val schema =
TopLevelObject(SourceMutation()),
TopLevelObject(TrackMutation()),
TopLevelObject(UpdateMutation()),
TopLevelObject(UserMutation()),
),
subscriptions =
listOf(

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.readValue
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.http.Header
import io.javalin.websocket.WsContext
import io.javalin.websocket.WsMessageContext
import kotlinx.coroutines.currentCoroutineContext
@@ -36,6 +37,8 @@ import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMess
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_ERROR
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ServerMessages.GQL_NEXT
import suwayomi.tachidesk.graphql.server.toGraphQLContext
import suwayomi.tachidesk.server.user.UserType
import suwayomi.tachidesk.server.user.getUserFromToken
/**
* Implementation of the `graphql-transport-ws` protocol defined by Denis Badurina
@@ -77,7 +80,7 @@ class ApolloSubscriptionProtocolHandler(
return try {
when (operationMessage.type) {
GQL_CONNECTION_INIT.type -> onInit(context)
GQL_CONNECTION_INIT.type -> onInit(operationMessage, context)
GQL_SUBSCRIBE.type -> startSubscription(operationMessage, context)
GQL_COMPLETE.type -> onComplete(operationMessage)
GQL_PING.type -> onPing()
@@ -144,17 +147,27 @@ class ApolloSubscriptionProtocolHandler(
}
}
private fun onInit(context: WsContext): Flow<SubscriptionOperationMessage> {
saveContext(context)
private fun onInit(
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)
}
/**
* Generate the context and save it for all future messages.
*/
private fun saveContext(context: WsContext) {
private fun saveContext(
user: UserType,
context: WsContext,
) {
runBlocking {
val graphQLContext = contextFactory.generateContextMap(context).toGraphQLContext()
val graphQLContext = contextFactory.generateContextMap(user, context).toGraphQLContext()
sessionState.saveContext(context, graphQLContext)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -10,7 +10,10 @@ import suwayomi.tachidesk.manga.impl.update.UpdateStatus
import suwayomi.tachidesk.manga.impl.update.UpdaterSocket
import suwayomi.tachidesk.manga.model.dataclass.MangaChapterDataClass
import suwayomi.tachidesk.manga.model.dataclass.PaginatedList
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.formParam
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
@@ -39,6 +42,7 @@ object UpdateController {
}
},
behaviorOf = { ctx, pageNum ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
ctx.future {
future {
Chapter.getRecentChapters(pageNum)
@@ -66,6 +70,7 @@ object UpdateController {
}
},
behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>()
if (categoryId == null) {
logger.info { "Adding Library to Update Queue" }
@@ -96,6 +101,7 @@ object UpdateController {
fun categoryUpdateWS(ws: WsConfig) {
ws.onConnect { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
UpdaterSocket.addClient(ctx)
}
ws.onMessage { ctx ->
@@ -115,6 +121,7 @@ object UpdateController {
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>()
ctx.json(updater.statusDeprecated.value)
},
@@ -132,6 +139,7 @@ object UpdateController {
}
},
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val updater = Injekt.get<IUpdater>()
logger.info { "Resetting Updater" }
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.PrimaryFilterType
import suwayomi.tachidesk.opds.impl.OpdsFeedBuilder
import suwayomi.tachidesk.server.JavalinSetup.Attribute
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
import suwayomi.tachidesk.server.user.requireUser
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
@@ -62,6 +65,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.contentType(OPDS_MIME).result(OpdsFeedBuilder.getRootFeed(BASE_URL, locale))
},
@@ -84,6 +88,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -109,6 +114,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.contentType("application/opensearchdescription+xml").result(
"""
@@ -136,6 +142,7 @@ object OpdsV1Controller {
handler(
documentWith = { withOperation { summary("OPDS Series in Library Feed") } },
behaviorOf = { ctx ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val pageNumber = ctx.queryParam("pageNumber")?.toIntOrNull()
val query = ctx.queryParam("query")
val author = ctx.queryParam("author")
@@ -188,6 +195,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -214,6 +222,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -240,6 +249,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -266,6 +276,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -291,6 +302,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -316,6 +328,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -342,6 +355,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, pageNumber, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -370,6 +384,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, sourceId, pageNumber, sort, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -405,6 +420,7 @@ object OpdsV1Controller {
pathParam<Long>("sourceId"),
documentWith = { withOperation { summary("OPDS Library Source Specific Series Feed") } },
behaviorOf = { ctx, sourceId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(sourceId = sourceId, primaryFilter = PrimaryFilterType.SOURCE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
},
@@ -422,6 +438,7 @@ object OpdsV1Controller {
pathParam<Int>("categoryId"),
documentWith = { withOperation { summary("OPDS Category Specific Series Feed") } },
behaviorOf = { ctx, categoryId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria =
buildCriteriaFromContext(ctx, OpdsMangaFilter(categoryId = categoryId, primaryFilter = PrimaryFilterType.CATEGORY))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
@@ -440,6 +457,7 @@ object OpdsV1Controller {
pathParam<String>("genre"),
documentWith = { withOperation { summary("OPDS Genre Specific Series Feed") } },
behaviorOf = { ctx, genre ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(genre = genre, primaryFilter = PrimaryFilterType.GENRE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
},
@@ -457,6 +475,7 @@ object OpdsV1Controller {
pathParam<Int>("statusId"),
documentWith = { withOperation { summary("OPDS Status Specific Series Feed") } },
behaviorOf = { ctx, statusId ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria = buildCriteriaFromContext(ctx, OpdsMangaFilter(statusId = statusId, primaryFilter = PrimaryFilterType.STATUS))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
},
@@ -479,6 +498,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, langCode ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val criteria =
buildCriteriaFromContext(ctx, OpdsMangaFilter(langCode = langCode, primaryFilter = PrimaryFilterType.LANGUAGE))
getLibraryFeed(ctx, ctx.queryParam("pageNumber")?.toIntOrNull(), criteria)
@@ -506,6 +526,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, seriesId, pageNumber, sort, filter, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {
@@ -536,6 +557,7 @@ object OpdsV1Controller {
}
},
behaviorOf = { ctx, seriesId, chapterIndex, lang ->
ctx.getAttribute(Attribute.TachideskUser).requireUser()
val locale: Locale = LocalizationHelper.ctxToLocale(ctx, lang)
ctx.future {
future {

View File

@@ -12,12 +12,14 @@ import gg.jte.TemplateEngine
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.http.Context
import io.javalin.http.HandlerType
import io.javalin.http.HttpStatus
import io.javalin.http.RedirectResponse
import io.javalin.http.UnauthorizedResponse
import io.javalin.http.staticfiles.Location
import io.javalin.rendering.template.JavalinJte
import io.javalin.websocket.WsContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -31,6 +33,11 @@ import suwayomi.tachidesk.graphql.types.AuthMode
import suwayomi.tachidesk.i18n.LocalizationHelper
import suwayomi.tachidesk.manga.MangaAPI
import suwayomi.tachidesk.opds.OpdsAPI
import suwayomi.tachidesk.server.user.ForbiddenException
import suwayomi.tachidesk.server.user.UnauthorizedException
import suwayomi.tachidesk.server.user.UserType
import suwayomi.tachidesk.server.user.getUserFromContext
import suwayomi.tachidesk.server.user.getUserFromWsContext
import suwayomi.tachidesk.server.util.Browser
import suwayomi.tachidesk.server.util.WebInterfaceManager
import uy.kohesive.injekt.injectLazy
@@ -207,6 +214,8 @@ object JavalinSetup {
ctx.header("WWW-Authenticate", "Basic")
throw UnauthorizedResponse()
}
ctx.setAttribute(Attribute.TachideskUser, getUserFromContext(ctx))
}
app.events { event ->
@@ -217,6 +226,12 @@ object JavalinSetup {
}
}
app.wsBefore {
it.onConnect { ctx ->
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
}
}
// when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
@@ -244,6 +259,18 @@ object JavalinSetup {
ctx.result(e.message ?: "Bad Request")
}
app.exception(UnauthorizedException::class.java) { e, ctx ->
logger.error(e) { "UnauthorizedException while handling the request" }
ctx.status(HttpStatus.UNAUTHORIZED)
ctx.result(e.message ?: "Unauthorized")
}
app.exception(ForbiddenException::class.java) { e, ctx ->
logger.error(e) { "ForbiddenException while handling the request" }
ctx.status(HttpStatus.FORBIDDEN)
ctx.result(e.message ?: "Forbidden")
}
app.start()
}
@@ -262,4 +289,28 @@ object JavalinSetup {
// )
// }
// }
sealed class Attribute<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.SystemPropertyOverridableConfigModule
import kotlin.reflect.KProperty
import kotlin.time.Duration
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
@@ -152,6 +153,9 @@ class ServerConfig(
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
val authUsername: 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({
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.generated.BuildConfig
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.DurationType
import suwayomi.tachidesk.server.util.MutableStateFlowType
import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt
@@ -176,6 +177,7 @@ fun applicationSetup() {
// register Tachidesk's config which is dubbed "ServerConfig"
registerCustomType(MutableStateFlowType())
registerCustomType(DurationType())
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
)

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
# Authentication
server.authMode = "none" # none, basic_auth or simple_login
server.authMode = "none" # none, basic_auth, simple_login or ui_login
server.authUsername = ""
server.authPassword = ""
server.jwtAudience = "suwayomi-server-api"
server.jwtTokenExpiry = "5m"
server.jwtRefreshExpiry = "60d"
# misc
server.debugLogsEnabled = false

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
# Authentication
server.authMode = "none" # none, basic_auth or simple_login
server.authMode = "none" # none, basic_auth, simple_login or ui_login
server.authUsername = ""
server.authPassword = ""
server.jwtAudience = "suwayomi-server-api"
server.jwtTokenExpiry = "5m"
server.jwtRefreshExpiry = "60d"
# misc
server.debugLogsEnabled = false