diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index 8b349ebce13..ed40441ea74 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -1004,6 +1004,7 @@ "ADMIN": "Administrator", "FILE_DOWNLOAD": "File download", "KOBO_SYNC": "Kobo Sync", + "KOREADER_SYNC": "KOReader Sync", "PAGE_STREAMING": "Page streaming", "USER": "User" }, diff --git a/komga-webui/src/types/enum-users.ts b/komga-webui/src/types/enum-users.ts index b385da99943..8ae924ea2de 100644 --- a/komga-webui/src/types/enum-users.ts +++ b/komga-webui/src/types/enum-users.ts @@ -2,7 +2,8 @@ export enum UserRoles { ADMIN = 'ADMIN', FILE_DOWNLOAD = 'FILE_DOWNLOAD', PAGE_STREAMING = 'PAGE_STREAMING', - KOBO_SYNC = 'KOBO_SYNC' + KOBO_SYNC = 'KOBO_SYNC', + KOREADER_SYNC = 'KOREADER_SYNC' } export enum AllowExclude { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/model/UserRoles.kt b/komga/src/main/kotlin/org/gotson/komga/domain/model/UserRoles.kt index f6f4b060f27..8d883bc8ec0 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/model/UserRoles.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/model/UserRoles.kt @@ -5,6 +5,7 @@ enum class UserRoles { FILE_DOWNLOAD, PAGE_STREAMING, KOBO_SYNC, + KOREADER_SYNC, ; companion object { diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt new file mode 100644 index 00000000000..7a7d9ec82b7 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/security/apikey/HeaderApiKeyAuthenticationConverter.kt @@ -0,0 +1,31 @@ +package org.gotson.komga.infrastructure.security.apikey + +import jakarta.servlet.http.HttpServletRequest +import org.gotson.komga.infrastructure.security.TokenEncoder +import org.springframework.security.authentication.AuthenticationDetailsSource +import org.springframework.security.core.Authentication +import org.springframework.security.web.authentication.AuthenticationConverter + +/** + * A strategy that retrieves the API key from the [headerName], + * and convert it to an [ApiKeyAuthenticationToken] + * + * @property headerName the header name from which to retrieve the API key + * @property tokenEncoder the encoder to use to encode the API key in the [Authentication] object + * @property authenticationDetailsSource the [AuthenticationDetailsSource] to enrich the [Authentication] details + */ +class HeaderApiKeyAuthenticationConverter( + private val headerName: String, + private val tokenEncoder: TokenEncoder, + private val authenticationDetailsSource: AuthenticationDetailsSource, +) : AuthenticationConverter { + override fun convert(request: HttpServletRequest): Authentication? = + request + .getHeader(headerName) + ?.let { + val (maskedToken, hashedToken) = it.take(6) + "*".repeat(6) to tokenEncoder.encode(it) + ApiKeyAuthenticationToken + .unauthenticated(maskedToken, hashedToken) + .apply { details = authenticationDetailsSource.buildDetails(request) } + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncController.kt new file mode 100644 index 00000000000..2228b8cd6f9 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncController.kt @@ -0,0 +1,43 @@ +package org.gotson.komga.interfaces.api.kosync + +import com.fasterxml.jackson.databind.JsonNode +import io.github.oshai.kotlinlogging.KotlinLogging +import org.gotson.komga.interfaces.api.kosync.dto.UserAuthenticationDto +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.server.ResponseStatusException + +private val logger = KotlinLogging.logger {} + +@RestController +@RequestMapping("/kosync", produces = [MediaType.APPLICATION_JSON_VALUE]) +class KoreaderSyncController { + @PostMapping("users/create") + fun registerUser(): Unit = throw ResponseStatusException(HttpStatus.FORBIDDEN) + + @GetMapping("users/auth") + fun authorize() = UserAuthenticationDto() + + @GetMapping("syncs/progress/{bookHash}") + fun getProgress( + @PathVariable bookHash: String, + ) { + logger.debug { "Received progress request for hash: $bookHash" } + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } + + @PutMapping("syncs/progress") + fun updateProgress( + @RequestBody body: JsonNode, + ) { + logger.debug { "Received progress update: $body" } + throw ResponseStatusException(HttpStatus.NOT_FOUND) + } +} diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/dto/UserAuthenticationDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/dto/UserAuthenticationDto.kt new file mode 100644 index 00000000000..557edd8fe14 --- /dev/null +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/kosync/dto/UserAuthenticationDto.kt @@ -0,0 +1,5 @@ +package org.gotson.komga.interfaces.api.kosync.dto + +data class UserAuthenticationDto( + val authorized: String = "OK", +) diff --git a/komga/src/test/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncControllerTest.kt b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncControllerTest.kt new file mode 100644 index 00000000000..2b68cda57f9 --- /dev/null +++ b/komga/src/test/kotlin/org/gotson/komga/interfaces/api/kosync/KoreaderSyncControllerTest.kt @@ -0,0 +1,71 @@ +package org.gotson.komga.interfaces.api.kosync + +import org.gotson.komga.domain.model.KomgaUser +import org.gotson.komga.domain.model.UserRoles +import org.gotson.komga.domain.persistence.KomgaUserRepository +import org.gotson.komga.domain.service.KomgaUserLifecycle +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.post + +@SpringBootTest +@AutoConfigureMockMvc(printOnlyOnFailure = false) +class KoreaderSyncControllerTest( + @Autowired private val mockMvc: MockMvc, + @Autowired private val userRepository: KomgaUserRepository, + @Autowired private val komgaUserLifecycle: KomgaUserLifecycle, +) { + private val user1 = + KomgaUser( + "user@example.org", + "", + roles = setOf(UserRoles.KOREADER_SYNC), + ) + private lateinit var apiKey: String + + @BeforeAll + fun setup() { + userRepository.insert(user1) + apiKey = komgaUserLifecycle.createApiKey(user1, "test")!!.key + } + + @AfterAll + fun teardown() { + komgaUserLifecycle.deleteUser(user1) + } + + @Test + fun `when creating user then forbidden is thrown`() { + mockMvc + .post("/kosync/users/create") + .andExpect { + status { isForbidden() } + } + } + + @Test + fun `given missing X-Auth-User header when authenticating user then forbidden is thrown`() { + mockMvc + .get("/kosync/users/auth") + .andExpect { + status { isForbidden() } + } + } + + @Test + fun `given api key in X-Auth-User header when authenticating user then returns OK`() { + mockMvc + .get("/kosync/users/auth") { + header("x-auth-user", apiKey) + }.andExpect { + status { isOk() } + jsonPath("authorized") { value("OK") } + } + } +}