Skip to content

Commit

Permalink
wip: kosync
Browse files Browse the repository at this point in the history
  • Loading branch information
gotson committed Jan 8, 2025
1 parent cbb0d61 commit f3ea932
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 1 deletion.
1 change: 1 addition & 0 deletions komga-webui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,7 @@
"ADMIN": "Administrator",
"FILE_DOWNLOAD": "File download",
"KOBO_SYNC": "Kobo Sync",
"KOREADER_SYNC": "KOReader Sync",
"PAGE_STREAMING": "Page streaming",
"USER": "User"
},
Expand Down
3 changes: 2 additions & 1 deletion komga-webui/src/types/enum-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum class UserRoles {
FILE_DOWNLOAD,
PAGE_STREAMING,
KOBO_SYNC,
KOREADER_SYNC,
;

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<HttpServletRequest, *>,
) : 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) }
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.gotson.komga.interfaces.api.kosync.dto

data class UserAuthenticationDto(
val authorized: String = "OK",
)
Original file line number Diff line number Diff line change
@@ -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(
"[email protected]",
"",
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") }
}
}
}

0 comments on commit f3ea932

Please sign in to comment.