diff --git a/komga/src/main/kotlin/org/gotson/komga/infrastructure/swagger/OpenApiConfiguration.kt b/komga/src/main/kotlin/org/gotson/komga/infrastructure/swagger/OpenApiConfiguration.kt index 0b1269fd3..fe87d5407 100644 --- a/komga/src/main/kotlin/org/gotson/komga/infrastructure/swagger/OpenApiConfiguration.kt +++ b/komga/src/main/kotlin/org/gotson/komga/infrastructure/swagger/OpenApiConfiguration.kt @@ -6,21 +6,86 @@ import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info import io.swagger.v3.oas.models.info.License import io.swagger.v3.oas.models.security.SecurityScheme +import io.swagger.v3.oas.models.tags.Tag +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.ANNOUNCEMENTS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.API_KEYS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOKS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_FONTS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_IMPORT +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_PAGES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_POSTER +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.BOOK_WEBPUB +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CLAIM +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CLIENT_SETTINGS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTIONS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTION_POSTER +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COLLECTION_SERIES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.COMICRACK +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.CURRENT_USER +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.DEPRECATED +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.DUPLICATE_PAGES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.FILE_SYSTEM +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.HISTORY +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.LIBRARIES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.LOGIN +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.MIHON +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.OAUTH2 +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLISTS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLIST_BOOKS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.READLIST_POSTER +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.REFERENTIAL +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.RELEASES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERIES +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERIES_POSTER +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SERVER_SETTINGS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.SYNCPOINTS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.TASKS +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames.USERS +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class OpenApiConfiguration { +class OpenApiConfiguration( + @Value("\${application.version}") private val appVersion: String, +) { @Bean fun openApi(): OpenAPI = OpenAPI() .info( Info() .title("Komga API") - .version("v1.0") + .version(appVersion) .description( """ Komga RESTful API. + + # Authentication + + Most endpoints require authentication. Authentication is done using either: + - Basic Authentication + - Passing an API Key in the `X-API-Key` header + + # Sessions + + Upon successful authentication, a session is created, and can be reused. + + - By default, a `SESSION` cookie is set via `Set-Cookie` response header. This works well for browsers and clients that can handle cookies. + - If you specify a header `X-Auth-Token` during authentication, the session ID will be returned via this same header. You can then pass that header again for subsequent requests to reuse the session. + + If you need to set the session cookie later on, you can call `/api/v1/login/set-cookie` with `X-Auth-Token`. The response will contain the `Set-Cookie` header. + + # Remember Me + + During authentication, if a request parameter `remember-me` is passed and set to `true`, the server will also return a `remember-me` cookie. This cookie will be used to login automatically even if the session has expired. + + # Logout + + You can explicitly logout an existing session by calling `/api/logout`. This would return a `204`. + + # Deprecation + + API endpoints marked as deprecated will be removed in the next major version. """.trimIndent(), ).license(License().name("MIT").url("https://github.com/gotson/komga/blob/master/LICENSE")), ).externalDocs( @@ -41,5 +106,183 @@ class OpenApiConfiguration { .`in`(SecurityScheme.In.HEADER) .name("X-API-Key"), ), - ) + ).tags(tags) + .extensions(mapOf("x-tagGroups" to tagGroups)) + + data class TagGroup( + val name: String, + val tags: List, + ) + + private val tagGroups = + listOf( + TagGroup( + "Libraries", + listOf( + LIBRARIES, + ), + ), + TagGroup( + "Series", + listOf( + SERIES, + SERIES_POSTER, + ), + ), + TagGroup( + "Books", + listOf( + BOOKS, + BOOK_PAGES, + BOOK_POSTER, + BOOK_IMPORT, + DUPLICATE_PAGES, + BOOK_WEBPUB, + BOOK_FONTS, + ), + ), + TagGroup( + "Collections", + listOf( + COLLECTIONS, + COLLECTION_SERIES, + COLLECTION_POSTER, + ), + ), + TagGroup( + "Readlists", + listOf( + READLISTS, + READLIST_BOOKS, + READLIST_POSTER, + ), + ), + TagGroup( + "Referential", + listOf( + REFERENTIAL, + ), + ), + TagGroup( + "Users", + listOf( + CURRENT_USER, + USERS, + API_KEYS, + LOGIN, + OAUTH2, + SYNCPOINTS, + ), + ), + TagGroup( + "Server", + listOf( + CLAIM, + SERVER_SETTINGS, + TASKS, + HISTORY, + FILE_SYSTEM, + RELEASES, + ANNOUNCEMENTS, + ), + ), + TagGroup( + "Integrations", + listOf( + CLIENT_SETTINGS, + MIHON, + COMICRACK, + ), + ), + TagGroup( + "Deprecation", + listOf( + DEPRECATED, + ), + ), + ) + + object TagNames { + const val LIBRARIES = "Libraries" + + const val SERIES = "Series" + const val SERIES_POSTER = "Series Poster" + + const val BOOKS = "Books" + const val BOOK_POSTER = "Book Poster" + const val BOOK_PAGES = "Book Pages" + const val BOOK_WEBPUB = "WebPub Manifest" + const val BOOK_IMPORT = "Import" + const val BOOK_FONTS = "Fonts" + const val DUPLICATE_PAGES = "Duplicate Pages" + + const val COLLECTIONS = "Collections" + const val COLLECTION_SERIES = "Collection Series" + const val COLLECTION_POSTER = "Collection Poster" + + const val READLISTS = "Readlists" + const val READLIST_BOOKS = "Readlist Books" + const val READLIST_POSTER = "Readlist Poster" + + const val REFERENTIAL = "Referential metadata" + + const val CURRENT_USER = "Current user" + const val USERS = "Users" + const val API_KEYS = "API Keys" + const val LOGIN = "User login" + const val OAUTH2 = "OAuth2" + const val SYNCPOINTS = "Sync points" + + const val CLAIM = "Claim server" + const val TASKS = "Tasks" + const val HISTORY = "History" + const val FILE_SYSTEM = "File system" + const val SERVER_SETTINGS = "Server settings" + const val RELEASES = "Releases" + const val ANNOUNCEMENTS = "Announcements" + + const val MIHON = "Mihon" + const val COMICRACK = "ComicRack" + const val CLIENT_SETTINGS = "Client settings" + + const val DEPRECATED = "Deprecated" + } + + private val tags = + listOf( + Tag().name(LIBRARIES).description("Manage libraries."), + Tag().name(SERIES).description("Manage series."), + Tag().name(SERIES_POSTER).description("Manage posters for series."), + Tag().name(BOOKS).description("Manage books."), + Tag().name(BOOK_POSTER).description("Manage posters for books."), + Tag().name(BOOK_PAGES), + Tag().name(BOOK_WEBPUB), + Tag().name(BOOK_IMPORT), + Tag().name(BOOK_FONTS).description("Provide font files and CSS for the Epub Reader."), + Tag().name(DUPLICATE_PAGES).description("Manage duplicate pages. Duplicate pages are identified by a page hash."), + Tag().name(COLLECTIONS).description("Manage collections."), + Tag().name(COLLECTION_POSTER).description("Manage posters for collections."), + Tag().name(COLLECTION_SERIES), + Tag().name(READLISTS).description("Manage readlists."), + Tag().name(READLIST_POSTER).description("Manage posters for readlists."), + Tag().name(READLIST_BOOKS), + Tag().name(REFERENTIAL).description("Retrieve referential metadata from all items in the Komga server."), + Tag().name(CURRENT_USER).description("Manage current user."), + Tag().name(USERS).description("Manage users."), + Tag().name(API_KEYS).description("Manage API Keys"), + Tag().name(LOGIN), + Tag().name(OAUTH2).description("List registered OAuth2 providers"), + Tag().name(SYNCPOINTS).description("Sync points are automatically created during a Kobo sync."), + Tag().name(CLAIM).description("Claim a freshly installed Komga server."), + Tag().name(TASKS).description("Manage server tasks"), + Tag().name(HISTORY).description("Server events history"), + Tag().name(FILE_SYSTEM).description("List files from the host server's file system"), + Tag().name(SERVER_SETTINGS).description("Store and retrieve server settings"), + Tag().name(RELEASES).description("Retrieve releases information"), + Tag().name(ANNOUNCEMENTS).description("Retrieve announcements from the Komga website"), + Tag().name(MIHON), + Tag().name(COMICRACK), + Tag().name(CLIENT_SETTINGS).description("Store and retrieve global and per-user settings. Those settings are not used by Komga itself, but can be stored for convenience by client applications."), + Tag().name(DEPRECATED), + ) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt index 13b6e609b..c469ba3b1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/CommonBookController.kt @@ -24,6 +24,7 @@ import org.gotson.komga.domain.service.BookLifecycle import org.gotson.komga.infrastructure.image.ImageType import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault import org.gotson.komga.interfaces.api.dto.MEDIATYPE_PROGRESSION_JSON_VALUE import org.gotson.komga.interfaces.api.persistence.BookDtoRepository @@ -190,6 +191,7 @@ class CommonBookController( } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get raw book page", description = "Returns the book page in raw format, without content negotiation.", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES]) @GetMapping( value = [ "api/v1/books/{bookId}/pages/{pageNumber}/raw", @@ -252,6 +254,7 @@ class CommonBookController( throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved") } + @Operation(summary = "Get Eoub resource", description = "Return a resource from within an Epub book.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = [ "api/v1/books/{bookId}/resource/{*resource}", @@ -306,7 +309,7 @@ class CommonBookController( .body(bytes) } - @Operation(description = "Download the book file.") + @Operation(summary = "Download book file", description = "Download the book file.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @GetMapping( value = [ "api/v1/books/{bookId}/file", @@ -360,6 +363,7 @@ class CommonBookController( } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get book progression", description = "The Progression API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = [ "api/v1/books/{bookId}/progression", @@ -379,6 +383,7 @@ class CommonBookController( } ?: ResponseEntity.noContent().build() } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Mark book progression", description = "The Progression API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @PutMapping( value = [ "api/v1/books/{bookId}/progression", diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/AnnouncementController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/AnnouncementController.kt index 6cb2df549..630063aae 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/AnnouncementController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/AnnouncementController.kt @@ -1,8 +1,11 @@ package org.gotson.komga.interfaces.api.rest import com.github.benmanes.caffeine.cache.Caffeine +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.domain.persistence.KomgaUserRepository import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.interfaces.api.rest.dto.JsonFeedDto import org.springframework.http.HttpStatus import org.springframework.http.MediaType @@ -22,6 +25,7 @@ private const val WEBSITE = "https://komga.org" @RestController @RequestMapping("api/v1/announcements", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.ANNOUNCEMENTS) class AnnouncementController( private val userRepository: KomgaUserRepository, webClientBuilder: WebClient.Builder, @@ -36,6 +40,7 @@ class AnnouncementController( @GetMapping @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Retrieve announcements") fun getAnnouncements( @AuthenticationPrincipal principal: KomgaPrincipal, ): JsonFeedDto = @@ -50,6 +55,7 @@ class AnnouncementController( @PreAuthorize("hasRole('ADMIN')") @PutMapping @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Mark announcements as read") fun markAnnouncementsRead( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestBody announcementIds: Set, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index cd405f1d5..5ce85f7cc 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -37,6 +37,7 @@ import org.gotson.komga.infrastructure.image.ImageAnalyzer import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault @@ -118,7 +119,7 @@ class BookController( @Deprecated("use /v1/books/list instead") @PageableAsQueryParam @GetMapping("api/v1/books") - @Operation(summary = "Use POST /api/v1/books/list instead") + @Operation(summary = "List books", description = "Use POST /api/v1/books/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.BOOKS, OpenApiConfiguration.TagNames.DEPRECATED]) fun getAllBooks( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", required = false) searchTerm: String? = null, @@ -170,6 +171,7 @@ class BookController( @PageableAsQueryParam @PostMapping("api/v1/books/list") + @Operation(summary = "List books", tags = [OpenApiConfiguration.TagNames.BOOKS]) fun getBooksList( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestBody search: BookSearch, @@ -198,7 +200,7 @@ class BookController( .map { it.restrictUrl(!principal.user.isAdmin) } } - @Operation(description = "Return newly added or updated books.") + @Operation(summary = "List latest books", description = "Return newly added or updated books.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PageableWithoutSortAsQueryParam @GetMapping("api/v1/books/latest") fun getLatestBooks( @@ -225,7 +227,7 @@ class BookController( ).map { it.restrictUrl(!principal.user.isAdmin) } } - @Operation(description = "Return first unread book of series with at least one book read and no books in progress.") + @Operation(summary = "List books on deck", description = "Return first unread book of series with at least one book read and no books in progress.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PageableWithoutSortAsQueryParam @GetMapping("api/v1/books/ondeck") fun getBooksOnDeck( @@ -241,6 +243,7 @@ class BookController( principal.user.restrictions, ).map { it.restrictUrl(!principal.user.isAdmin) } + @Operation(summary = "List duplicate books", description = "Return books that have the same file hash.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PageableAsQueryParam @GetMapping("api/v1/books/duplicates") @PreAuthorize("hasRole('ADMIN')") @@ -268,6 +271,7 @@ class BookController( return bookDtoRepository.findAllDuplicates(principal.user.id, pageRequest) } + @Operation(summary = "Get book details", tags = [OpenApiConfiguration.TagNames.BOOKS]) @GetMapping("api/v1/books/{bookId}") fun getOneBook( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -279,6 +283,7 @@ class BookController( it.restrictUrl(!principal.user.isAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get previous book in series", tags = [OpenApiConfiguration.TagNames.BOOKS]) @GetMapping("api/v1/books/{bookId}/previous") fun getBookSiblingPrevious( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -292,6 +297,7 @@ class BookController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Get next book in series", tags = [OpenApiConfiguration.TagNames.BOOKS]) @GetMapping("api/v1/books/{bookId}/next") fun getBookSiblingNext( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -305,6 +311,7 @@ class BookController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List book's readlists", tags = [OpenApiConfiguration.TagNames.BOOKS]) @GetMapping("api/v1/books/{bookId}/readlists") fun getAllReadListsByBook( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -317,6 +324,7 @@ class BookController( .map { it.toDto() } } + @Operation(summary = "Get book's poster image", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping( value = ["api/v1/books/{bookId}/thumbnail"], @@ -331,6 +339,7 @@ class BookController( return bookLifecycle.getThumbnailBytes(bookId)?.bytes ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Get book poster image", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["api/v1/books/{bookId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getBookThumbnailById( @@ -344,6 +353,7 @@ class BookController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List book posters", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @GetMapping(value = ["api/v1/books/{bookId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE]) fun getBookThumbnails( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -356,6 +366,7 @@ class BookController( .map { it.toDto() } } + @Operation(summary = "Add book poster", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @PostMapping(value = ["api/v1/books/{bookId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasRole('ADMIN')") fun addUserUploadedBookThumbnail( @@ -385,6 +396,7 @@ class BookController( ).toDto() } + @Operation(summary = "Mark book poster as selected", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @PutMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}/selected") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -399,6 +411,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete book poster", description = "Only uploaded posters can be deleted.", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @DeleteMapping("api/v1/books/{bookId}/thumbnails/{thumbnailId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -416,6 +429,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List book pages", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES]) @GetMapping("api/v1/books/{bookId}/pages") fun getBookPages( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -450,6 +464,7 @@ class BookController( } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get book page image", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES]) @ApiResponse(content = [Content(mediaType = "image/*", schema = Schema(type = "string", format = "binary"))]) @GetMapping( value = ["api/v1/books/{bookId}/pages/{pageNumber}"], @@ -477,6 +492,7 @@ class BookController( contentNegotiation: Boolean, ): ResponseEntity = commonBookController.getBookPageInternal(bookId, if (zeroBasedIndex) pageNumber + 1 else pageNumber, convertTo, request, principal, if (contentNegotiation) acceptHeaders else null) + @Operation(summary = "Get book page thumbnail", description = "The image is resized to 300px on the largest dimension.", tags = [OpenApiConfiguration.TagNames.BOOK_PAGES]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping( value = ["api/v1/books/{bookId}/pages/{pageNumber}/thumbnail"], @@ -519,6 +535,7 @@ class BookController( } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get book's WebPub manifest", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = ["api/v1/books/{bookId}/manifest"], produces = [MEDIATYPE_WEBPUB_JSON_VALUE, MEDIATYPE_DIVINA_JSON_VALUE], @@ -534,6 +551,7 @@ class BookController( .body(manifest) } + @Operation(summary = "List book's positions", description = "The Positions API is a proposed standard for OPDS 2 and Readium. It is used by the Epub Reader.", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = ["api/v1/books/{bookId}/positions"], produces = [MEDIATYPE_POSITION_LIST_JSON_VALUE], @@ -566,6 +584,7 @@ class BookController( .body(R2Positions(extension.positions.size, extension.positions)) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get book's WebPub manifest (Epub)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = ["api/v1/books/{bookId}/manifest/epub"], produces = [MEDIATYPE_WEBPUB_JSON_VALUE], @@ -575,6 +594,7 @@ class BookController( @PathVariable bookId: String, ): WPPublicationDto = commonBookController.getWebPubManifestEpubInternal(principal, bookId, webPubGenerator) + @Operation(summary = "Get book's WebPub manifest (PDF)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = ["api/v1/books/{bookId}/manifest/pdf"], produces = [MEDIATYPE_WEBPUB_JSON_VALUE], @@ -584,6 +604,7 @@ class BookController( @PathVariable bookId: String, ): WPPublicationDto = commonBookController.getWebPubManifestPdfInternal(principal, bookId, webPubGenerator) + @Operation(summary = "Get book's WebPub manifest (DiViNa)", tags = [OpenApiConfiguration.TagNames.BOOK_WEBPUB]) @GetMapping( value = ["api/v1/books/{bookId}/manifest/divina"], produces = [MEDIATYPE_DIVINA_JSON_VALUE], @@ -593,6 +614,7 @@ class BookController( @PathVariable bookId: String, ): WPPublicationDto = commonBookController.getWebPubManifestDivinaInternal(principal, bookId, webPubGenerator) + @Operation(summary = "Analyze book", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PostMapping("api/v1/books/{bookId}/analyze") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -604,6 +626,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Refresh book metadata", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PostMapping("api/v1/books/{bookId}/metadata/refresh") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -616,6 +639,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Update book metadata", description = "Set a field to null to unset the metadata. You can omit fields you don't want to update.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PatchMapping("api/v1/books/{bookId}/metadata") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -635,6 +659,7 @@ class BookController( } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Update book metadata in bulk", description = "Set a field to null to unset the metadata. You can omit fields you don't want to update.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PatchMapping("api/v1/books/metadata") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -658,7 +683,7 @@ class BookController( updatedBooks.map { it.seriesId }.distinct().forEach { taskEmitter.aggregateSeriesMetadata(it) } } - @Operation(description = "Mark book as read and/or change page progress") + @Operation(summary = "Mark book's read progress", description = "Mark book as read and/or change page progress.", tags = [OpenApiConfiguration.TagNames.BOOKS]) @PatchMapping("api/v1/books/{bookId}/read-progress") @ResponseStatus(HttpStatus.NO_CONTENT) fun markReadProgress( @@ -683,7 +708,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } - @Operation(description = "Mark book as unread") + @Operation(summary = "Mark book as unread", description = "Mark book as unread", tags = [OpenApiConfiguration.TagNames.BOOKS]) @DeleteMapping("api/v1/books/{bookId}/read-progress") @ResponseStatus(HttpStatus.NO_CONTENT) fun deleteReadProgress( @@ -697,6 +722,7 @@ class BookController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Import books", tags = [OpenApiConfiguration.TagNames.BOOK_IMPORT]) @PostMapping("api/v1/books/import") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -719,10 +745,11 @@ class BookController( } } + @Operation(summary = "Delete book file", tags = [OpenApiConfiguration.TagNames.BOOKS]) @DeleteMapping("api/v1/books/{bookId}/file") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) - fun deleteBook( + fun deleteBookFile( @PathVariable bookId: String, ) { taskEmitter.deleteBook( @@ -731,6 +758,7 @@ class BookController( ) } + @Operation(summary = "Regenerate books posters", tags = [OpenApiConfiguration.TagNames.BOOK_POSTER]) @PutMapping("api/v1/books/thumbnails") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClaimController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClaimController.kt index c6c391622..fbf427e2d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClaimController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClaimController.kt @@ -1,10 +1,13 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.constraints.Email import jakarta.validation.constraints.NotBlank import org.gotson.komga.domain.model.KomgaUser import org.gotson.komga.domain.model.UserRoles import org.gotson.komga.domain.service.KomgaUserLifecycle +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.interfaces.api.rest.dto.UserDto import org.gotson.komga.interfaces.api.rest.dto.toDto import org.springframework.http.HttpStatus @@ -19,14 +22,17 @@ import org.springframework.web.server.ResponseStatusException @RestController @RequestMapping("api/v1/claim", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.CLAIM) @Validated class ClaimController( private val userDetailsLifecycle: KomgaUserLifecycle, ) { @GetMapping + @Operation(summary = "Retrieve claim status", description = "Check whether this server has already been claimed.") fun getClaimStatus() = ClaimStatus(userDetailsLifecycle.countUsers() > 0) @PostMapping + @Operation(summary = "Claim server", description = "Creates an admin user with the provided credentials.") fun claimAdmin( @Email(regexp = ".+@.+\\..+") @RequestHeader("X-Komga-Email") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClientSettingsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClientSettingsController.kt index 16e799927..b82e6e52d 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClientSettingsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ClientSettingsController.kt @@ -9,6 +9,7 @@ import jakarta.validation.constraints.NotNull import jakarta.validation.constraints.Pattern import org.gotson.komga.infrastructure.jooq.main.ClientSettingsDtoDao import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.interfaces.api.rest.dto.ClientSettingDto import org.gotson.komga.interfaces.api.rest.dto.ClientSettingGlobalUpdateDto import org.gotson.komga.interfaces.api.rest.dto.ClientSettingUserUpdateDto @@ -30,13 +31,7 @@ private const val KEY_REGEX = """^[a-z](?:[a-z0-9_-]*[a-z0-9])*(?:\.[a-z0-9](?:[ @RestController @RequestMapping(value = ["api/v1/client-settings"], produces = [MediaType.APPLICATION_JSON_VALUE]) -@Tag( - name = "Client Settings", - description = """ - Store and retrieve global and per-user settings. - Those settings are not used by Komga itself, but can be stored for convenience by client applications. - """, -) +@Tag(name = OpenApiConfiguration.TagNames.CLIENT_SETTINGS) @Validated class ClientSettingsController( private val clientSettingsDtoDao: ClientSettingsDtoDao, @@ -48,7 +43,7 @@ class ClientSettingsController( ): Map = clientSettingsDtoDao.findAllGlobal(principal == null) @GetMapping("user/list") - @Operation(summary = "Retrieve client settings for the current user") + @Operation(summary = "Retrieve user client settings") fun getUserSettings( @AuthenticationPrincipal principal: KomgaPrincipal, ): Map = clientSettingsDtoDao.findAllUser(principal.user.id) @@ -95,7 +90,7 @@ class ClientSettingsController( @PatchMapping("user") @ResponseStatus(HttpStatus.NO_CONTENT) - @Operation(summary = "Save user settings for the current user", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'") + @Operation(summary = "Save user settings", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'") @OASRequestBody( content = [ Content( @@ -155,7 +150,7 @@ class ClientSettingsController( @DeleteMapping("user") @ResponseStatus(HttpStatus.NO_CONTENT) - @Operation(summary = "Delete user settings for the current user", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'") + @Operation(summary = "Delete user settings", description = "Setting key should be a valid lowercase namespace string like 'application.domain.key'") @OASRequestBody( content = [ Content( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FileSystemController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FileSystemController.kt index 8af454c2e..e2cc75737 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FileSystemController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FileSystemController.kt @@ -1,6 +1,9 @@ package org.gotson.komga.interfaces.api.rest import com.fasterxml.jackson.annotation.JsonInclude +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -18,10 +21,15 @@ import kotlin.streams.asSequence @RestController @RequestMapping("api/v1/filesystem", produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("hasRole('ADMIN')") +@Tag(name = OpenApiConfiguration.TagNames.FILE_SYSTEM) class FileSystemController { private val fs = FileSystems.getDefault() @PostMapping + @Operation( + summary = "Directory listing", + description = "List folders and files from the host server's file system. If no request body is passed then the root directories are returned.", + ) fun getDirectoryListing( @RequestBody(required = false) request: DirectoryRequestDto = DirectoryRequestDto(), ): DirectoryListingDto = diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FontsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FontsController.kt index b0e902ba7..7f732f35c 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FontsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/FontsController.kt @@ -1,7 +1,10 @@ package org.gotson.komga.interfaces.api.rest import io.github.oshai.kotlinlogging.KotlinLogging +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.infrastructure.configuration.KomgaProperties +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.language.contains import org.springframework.core.io.ByteArrayResource import org.springframework.core.io.FileSystemResource @@ -28,6 +31,7 @@ private val logger = KotlinLogging.logger {} @RestController @RequestMapping(value = ["api/v1/fonts"], produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.BOOK_FONTS) class FontsController( komgaProperties: KomgaProperties, ) { @@ -81,9 +85,11 @@ class FontsController( } @GetMapping("families") + @Operation(summary = "List font families", description = "List all available font families.") fun listFonts(): Set = fonts.keys @GetMapping("resource/{fontFamily}/{fontFile}") + @Operation(summary = "Download font file") fun getFontFile( @PathVariable fontFamily: String, @PathVariable fontFile: String, @@ -105,6 +111,7 @@ class FontsController( } @GetMapping("resource/{fontFamily}/css", produces = ["text/css"]) + @Operation(summary = "Download CSS file", description = "Download a CSS file with the @font-face block for the font family. This is used by the Epub Reader to change fonts.") fun getFontFamilyAsCss( @PathVariable fontFamily: String, ): ResponseEntity { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/HistoricalEventController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/HistoricalEventController.kt index c88716337..1df7cccee 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/HistoricalEventController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/HistoricalEventController.kt @@ -1,6 +1,9 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam import org.gotson.komga.interfaces.api.persistence.HistoricalEventDtoRepository import org.gotson.komga.interfaces.api.rest.dto.HistoricalEventDto @@ -17,11 +20,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("api/v1/history", produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("hasRole('ADMIN')") +@Tag(name = OpenApiConfiguration.TagNames.HISTORY) class HistoricalEventController( private val historicalEventDtoRepository: HistoricalEventDtoRepository, ) { @GetMapping @PageableAsQueryParam + @Operation(summary = "List historical events") fun getAll( @Parameter(hidden = true) page: Pageable, ): Page { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt index 60e21ebca..3d7aeafd6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LibraryController.kt @@ -2,6 +2,7 @@ package org.gotson.komga.interfaces.api.rest import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.gotson.komga.application.tasks.HIGHEST_PRIORITY import org.gotson.komga.application.tasks.HIGH_PRIORITY @@ -18,6 +19,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.persistence.SeriesRepository import org.gotson.komga.domain.service.LibraryLifecycle import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.web.filePathToUrl import org.gotson.komga.interfaces.api.rest.dto.LibraryCreationDto import org.gotson.komga.interfaces.api.rest.dto.LibraryDto @@ -45,6 +47,7 @@ import java.io.FileNotFoundException @RestController @RequestMapping("api/v1/libraries", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.LIBRARIES) class LibraryController( private val taskEmitter: TaskEmitter, private val libraryLifecycle: LibraryLifecycle, @@ -53,6 +56,10 @@ class LibraryController( private val seriesRepository: SeriesRepository, ) { @GetMapping + @Operation( + summary = "List all libraries", + description = "The libraries are filtered based on the current user's permissions", + ) fun getAll( @AuthenticationPrincipal principal: KomgaPrincipal, ): List = @@ -63,6 +70,7 @@ class LibraryController( }.sortedBy { it.name.lowercase() }.map { it.toDto(includeRoot = principal.user.isAdmin) } @GetMapping("{libraryId}") + @Operation(summary = "Get details for a single library") fun getOne( @AuthenticationPrincipal principal: KomgaPrincipal, @PathVariable libraryId: String, @@ -74,6 +82,7 @@ class LibraryController( @PostMapping @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Create a library") fun addOne( @AuthenticationPrincipal principal: KomgaPrincipal, @Valid @RequestBody @@ -130,7 +139,7 @@ class LibraryController( @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @Deprecated("Use PATCH /v1/libraries/{libraryId} instead", ReplaceWith("patchOne")) - @Operation(summary = "Use PATCH /api/v1/libraries/{libraryId} instead") + @Operation(summary = "Update a library", description = "Use PATCH /api/v1/libraries/{libraryId} instead. Deprecated since 1.3.0.", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun updateOne( @PathVariable libraryId: String, @Valid @RequestBody @@ -142,6 +151,7 @@ class LibraryController( @PatchMapping("/{libraryId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Update a library", description = "You can omit fields you don't want to update") fun patchOne( @PathVariable libraryId: String, @Parameter(description = "Fields to update. You can omit fields you don't want to update.") @@ -204,6 +214,7 @@ class LibraryController( @DeleteMapping("/{libraryId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a library") fun deleteOne( @PathVariable libraryId: String, ) { @@ -215,6 +226,7 @@ class LibraryController( @PostMapping("{libraryId}/scan") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) + @Operation(summary = "Scan a library") fun scan( @PathVariable libraryId: String, @RequestParam(required = false) deep: Boolean = false, @@ -227,6 +239,7 @@ class LibraryController( @PostMapping("{libraryId}/analyze") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) + @Operation(summary = "Analyze a library") fun analyze( @PathVariable libraryId: String, ) { @@ -243,6 +256,7 @@ class LibraryController( @PostMapping("{libraryId}/metadata/refresh") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) + @Operation(summary = "Refresh metadata for a library") fun refreshMetadata( @PathVariable libraryId: String, ) { @@ -261,6 +275,7 @@ class LibraryController( @PostMapping("{libraryId}/empty-trash") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) + @Operation(summary = "Empty trash for a library") fun emptyTrash( @PathVariable libraryId: String, ) { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LoginController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LoginController.kt index f6b707cd9..ea9587cc9 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LoginController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/LoginController.kt @@ -1,8 +1,11 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpSession +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.session.web.http.CookieSerializer @@ -13,9 +16,11 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.LOGIN) class LoginController( private val cookieSerializer: CookieSerializer, ) { + @Operation(summary = "Set cookie", description = "Forcefully return Set-Cookie header, even if the session is contained in the X-Auth-Token header.") @GetMapping("api/v1/login/set-cookie") @ResponseStatus(HttpStatus.NO_CONTENT) fun headerToCookie( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/OAuth2Controller.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/OAuth2Controller.kt index affe4bf9c..e610f2fb5 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/OAuth2Controller.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/OAuth2Controller.kt @@ -1,5 +1,8 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.springframework.http.MediaType import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository import org.springframework.web.bind.annotation.GetMapping @@ -8,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("api/v1/oauth2", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.OAUTH2) class OAuth2Controller( clientRegistrationRepository: InMemoryClientRegistrationRepository?, ) { @@ -17,6 +21,7 @@ class OAuth2Controller( } ?: emptyList() @GetMapping("providers") + @Operation(summary = "List registered OAuth2 providers") fun getProviders() = registrationIds } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt index 2d7f8aff1..f5395c6ba 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/PageHashController.kt @@ -1,15 +1,18 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.gotson.komga.application.tasks.TaskEmitter import org.gotson.komga.domain.model.BookPageNumbered import org.gotson.komga.domain.model.PageHashKnown import org.gotson.komga.domain.persistence.PageHashRepository import org.gotson.komga.domain.service.PageHashLifecycle +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault import org.gotson.komga.interfaces.api.rest.dto.PageHashCreationDto @@ -37,11 +40,13 @@ import org.springframework.web.server.ResponseStatusException @RestController @RequestMapping("api/v1/page-hashes", produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("hasRole('ADMIN')") +@Tag(name = OpenApiConfiguration.TagNames.DUPLICATE_PAGES) class PageHashController( private val pageHashRepository: PageHashRepository, private val pageHashLifecycle: PageHashLifecycle, private val taskEmitter: TaskEmitter, ) { + @Operation(summary = "List known duplicates") @GetMapping @PageableAsQueryParam fun getKnownPageHashes( @@ -49,6 +54,7 @@ class PageHashController( @Parameter(hidden = true) page: Pageable, ): Page = pageHashRepository.findAllKnown(actions, page).map { it.toDto() } + @Operation(summary = "Get known duplicate image thumbnail") @GetMapping("/{pageHash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) fun getKnownPageHashThumbnail( @@ -57,12 +63,14 @@ class PageHashController( pageHashRepository.getKnownThumbnail(pageHash) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "List unknown duplicates") @GetMapping("/unknown") @PageableAsQueryParam fun getUnknownPageHashes( @Parameter(hidden = true) page: Pageable, ): Page = pageHashRepository.findAllUnknown(page).map { it.toDto() } + @Operation(summary = "List duplicate matches") @GetMapping("{pageHash}") @PageableAsQueryParam fun getPageHashMatches( @@ -75,6 +83,7 @@ class PageHashController( page, ).map { it.toDto() } + @Operation(summary = "Get unknown duplicate image thumbnail") @GetMapping("unknown/{pageHash}/thumbnail", produces = [MediaType.IMAGE_JPEG_VALUE]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) fun getUnknownPageHashThumbnail( @@ -88,6 +97,7 @@ class PageHashController( .body(it.bytes) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Mark duplicate page as known") @PutMapping @ResponseStatus(HttpStatus.ACCEPTED) fun createOrUpdateKnownPageHash( @@ -107,6 +117,7 @@ class PageHashController( } } + @Operation(summary = "Delete all duplicate pages by hash") @PostMapping("{pageHash}/delete-all") @ResponseStatus(HttpStatus.ACCEPTED) fun performDelete( @@ -131,6 +142,7 @@ class PageHashController( taskEmitter.removeDuplicatePages(toRemove) } + @Operation(summary = "Delete specific duplicate page") @PostMapping("{pageHash}/delete-match") @ResponseStatus(HttpStatus.ACCEPTED) fun deleteSingleMatch( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt index 2e6ce65ef..9a870fd32 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt @@ -1,6 +1,7 @@ package org.gotson.komga.interfaces.api.rest import io.github.oshai.kotlinlogging.KotlinLogging +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -34,6 +35,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.infrastructure.web.Authors import org.gotson.komga.interfaces.api.persistence.BookDtoRepository @@ -98,6 +100,7 @@ class ReadListController( private val bookLifecycle: BookLifecycle, private val eventPublisher: ApplicationEventPublisher, ) { + @Operation(summary = "List readlists", tags = [OpenApiConfiguration.TagNames.READLISTS]) @PageableWithoutSortAsQueryParam @GetMapping fun getAll( @@ -129,6 +132,7 @@ class ReadListController( .map { it.toDto() } } + @Operation(summary = "Get readlist details", tags = [OpenApiConfiguration.TagNames.READLISTS]) @GetMapping("{id}") fun getOne( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -139,6 +143,7 @@ class ReadListController( ?.toDto() ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get readlist's poster image", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["{id}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getReadListThumbnail( @@ -153,6 +158,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Get readlist poster image", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getReadListThumbnailById( @@ -166,6 +172,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List readlist's posters", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE]) fun getReadListThumbnails( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -176,6 +183,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Add readlist poster", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasRole('ADMIN')") fun addUserUploadedReadListThumbnail( @@ -205,6 +213,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Mark readlist poster as selected", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @PutMapping("{id}/thumbnails/{thumbnailId}/selected") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -221,6 +230,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete readlist poster", tags = [OpenApiConfiguration.TagNames.READLIST_POSTER]) @DeleteMapping("{id}/thumbnails/{thumbnailId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -236,6 +246,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Create readlist", tags = [OpenApiConfiguration.TagNames.READLISTS]) @PostMapping @PreAuthorize("hasRole('ADMIN')") fun addOne( @@ -256,6 +267,7 @@ class ReadListController( throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) } + @Operation(summary = "Match ComicRack list", tags = [OpenApiConfiguration.TagNames.COMICRACK]) @PostMapping("match/comicrack") @PreAuthorize("hasRole('ADMIN')") fun matchFromComicRackList( @@ -267,6 +279,7 @@ class ReadListController( throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.code) } + @Operation(summary = "Update readlist", tags = [OpenApiConfiguration.TagNames.READLISTS]) @PatchMapping("{id}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -291,6 +304,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete readlist", tags = [OpenApiConfiguration.TagNames.READLISTS]) @DeleteMapping("{id}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -302,6 +316,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List readlist's books", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS]) @PageableWithoutSortAsQueryParam @AuthorsAsQueryParam @GetMapping("{id}/books") @@ -353,6 +368,7 @@ class ReadListController( .map { it.restrictUrl(!principal.user.isAdmin) } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get previous book in readlist", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS]) @GetMapping("{id}/books/{bookId}/previous") fun getBookSiblingPrevious( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -370,6 +386,7 @@ class ReadListController( )?.restrictUrl(!principal.user.isAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get next book in readlist", tags = [OpenApiConfiguration.TagNames.READLIST_BOOKS]) @GetMapping("{id}/books/{bookId}/next") fun getBookSiblingNext( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -387,6 +404,7 @@ class ReadListController( )?.restrictUrl(!principal.user.isAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get readlist read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON]) @GetMapping("{id}/read-progress/tachiyomi") fun getReadProgress( @PathVariable id: String, @@ -396,6 +414,7 @@ class ReadListController( readProgressDtoRepository.findProgressByReadList(readList.id, principal.user.id) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Update readlist read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON]) @PutMapping("{id}/read-progress/tachiyomi") @ResponseStatus(HttpStatus.NO_CONTENT) fun markReadProgressTachiyomi( @@ -415,6 +434,7 @@ class ReadListController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Download readlist", description = "Download the whole readlist as a ZIP file.", tags = [OpenApiConfiguration.TagNames.READLISTS]) @GetMapping("{id}/file", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) @PreAuthorize("hasRole('FILE_DOWNLOAD')") fun getReadListFile( diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialController.kt index 84c5c5d17..d3ceaef45 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReferentialController.kt @@ -1,8 +1,11 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.domain.persistence.ReferentialRepository import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.interfaces.api.rest.dto.AuthorDto import org.gotson.komga.interfaces.api.rest.dto.toDto @@ -18,10 +21,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("api", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.REFERENTIAL) class ReferentialController( private val referentialRepository: ReferentialRepository, ) { @GetMapping("v1/authors") + @Deprecated("Use GET /v2/authors instead", ReplaceWith("getAuthors")) + @Operation(summary = "List authors", description = "Use GET /api/v2/authors instead. Deprecated since 1.20.0.", tags = [OpenApiConfiguration.TagNames.DEPRECATED]) fun getAuthorsV1( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", defaultValue = "") search: String, @@ -39,6 +45,7 @@ class ReferentialController( @PageableWithoutSortAsQueryParam @GetMapping("v2/authors") + @Operation(summary = "List authors", description = "Can be filtered by various criteria") fun getAuthors( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", required = false) search: String?, @@ -69,17 +76,20 @@ class ReferentialController( } @GetMapping("v1/authors/names") + @Operation(summary = "List authors' names") fun getAuthorsNames( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", defaultValue = "") search: String, ): List = referentialRepository.findAllAuthorsNamesByName(search, principal.user.getAuthorizedLibraryIds(null)) @GetMapping("v1/authors/roles") + @Operation(summary = "List authors' roles") fun getAuthorsRoles( @AuthenticationPrincipal principal: KomgaPrincipal, ): List = referentialRepository.findAllAuthorsRoles(principal.user.getAuthorizedLibraryIds(null)) @GetMapping("v1/genres") + @Operation(summary = "List genres", description = "Can be filtered by various criteria") fun getGenres( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -92,6 +102,7 @@ class ReferentialController( } @GetMapping("v1/sharing-labels") + @Operation(summary = "List sharing labels", description = "Can be filtered by various criteria") fun getSharingLabels( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -104,6 +115,7 @@ class ReferentialController( } @GetMapping("v1/tags") + @Operation(summary = "List tags", description = "Can be filtered by various criteria") fun getTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -116,6 +128,7 @@ class ReferentialController( } @GetMapping("v1/tags/book") + @Operation(summary = "List book tags", description = "Can be filtered by various criteria") fun getBookTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "series_id", required = false) seriesId: String?, @@ -128,6 +141,7 @@ class ReferentialController( } @GetMapping("v1/tags/series") + @Operation(summary = "List series tags", description = "Can be filtered by various criteria") fun getSeriesTags( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -140,6 +154,7 @@ class ReferentialController( } @GetMapping("v1/languages") + @Operation(summary = "List languages", description = "Can be filtered by various criteria") fun getLanguages( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -152,6 +167,7 @@ class ReferentialController( } @GetMapping("v1/publishers") + @Operation(summary = "List publishers", description = "Can be filtered by various criteria") fun getPublishers( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -164,6 +180,7 @@ class ReferentialController( } @GetMapping("v1/age-ratings") + @Operation(summary = "List age ratings", description = "Can be filtered by various criteria") fun getAgeRatings( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, @@ -176,6 +193,7 @@ class ReferentialController( }.map { it?.toString() ?: "None" }.toSet() @GetMapping("v1/series/release-dates") + @Operation(summary = "List series release dates", description = "Can be filtered by various criteria") fun getSeriesReleaseDates( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "library_id", required = false) libraryId: String?, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReleaseController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReleaseController.kt index 38570b7d0..141fdf228 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReleaseController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReleaseController.kt @@ -1,7 +1,10 @@ package org.gotson.komga.interfaces.api.rest import com.github.benmanes.caffeine.cache.Caffeine +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.interfaces.api.rest.dto.GithubReleaseDto import org.gotson.komga.interfaces.api.rest.dto.ReleaseDto import org.springframework.core.ParameterizedTypeReference @@ -21,6 +24,7 @@ private const val GITHUB_API = "https://api.github.com/repos/gotson/komga/releas @RestController @PreAuthorize("hasRole('ADMIN')") @RequestMapping("api/v1/releases", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.RELEASES) class ReleaseController( webClientBuilder: WebClient.Builder, ) { @@ -34,7 +38,8 @@ class ReleaseController( @GetMapping @PreAuthorize("hasRole('ADMIN')") - fun getAnnouncements( + @Operation(summary = "List releases") + fun getReleases( @AuthenticationPrincipal principal: KomgaPrincipal, ): List = cache diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt index a9491717f..2dd4def4b 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt @@ -1,6 +1,7 @@ package org.gotson.komga.interfaces.api.rest import io.github.oshai.kotlinlogging.KotlinLogging +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema @@ -26,6 +27,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.infrastructure.web.Authors import org.gotson.komga.interfaces.api.persistence.SeriesDtoRepository @@ -77,6 +79,7 @@ class SeriesCollectionController( private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository, private val eventPublisher: ApplicationEventPublisher, ) { + @Operation(summary = "List collections", tags = [OpenApiConfiguration.TagNames.COLLECTIONS]) @PageableWithoutSortAsQueryParam @GetMapping fun getAll( @@ -107,6 +110,7 @@ class SeriesCollectionController( .map { it.toDto() } } + @Operation(summary = "Get collection details", tags = [OpenApiConfiguration.TagNames.COLLECTIONS]) @GetMapping("{id}") fun getOne( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -117,6 +121,7 @@ class SeriesCollectionController( ?.toDto() ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get collection's poster image", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["{id}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getCollectionThumbnail( @@ -131,6 +136,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Get collection poster image", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["{id}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getCollectionThumbnailById( @@ -144,6 +150,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List collection's posters", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @GetMapping(value = ["{id}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE]) fun getCollectionThumbnails( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -154,6 +161,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Add collection poster", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @PostMapping(value = ["{id}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasRole('ADMIN')") fun addUserUploadedCollectionThumbnail( @@ -183,6 +191,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Mark collection poster as selected", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @PutMapping("{id}/thumbnails/{thumbnailId}/selected") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -199,6 +208,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete collection poster", tags = [OpenApiConfiguration.TagNames.COLLECTION_POSTER]) @DeleteMapping("{id}/thumbnails/{thumbnailId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -214,6 +224,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Create collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS]) @PostMapping @PreAuthorize("hasRole('ADMIN')") fun addOne( @@ -233,6 +244,7 @@ class SeriesCollectionController( throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.message) } + @Operation(summary = "Update collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS]) @PatchMapping("{id}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -256,6 +268,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete collection", tags = [OpenApiConfiguration.TagNames.COLLECTIONS]) @DeleteMapping("{id}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -267,6 +280,7 @@ class SeriesCollectionController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List collection's series", tags = [OpenApiConfiguration.TagNames.COLLECTION_SERIES]) @PageableWithoutSortAsQueryParam @AuthorsAsQueryParam @GetMapping("{id}/series") diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt index de5e712e3..f5200ad54 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt @@ -46,6 +46,7 @@ import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.mediacontainer.ContentDetector import org.gotson.komga.infrastructure.security.KomgaPrincipal import org.gotson.komga.infrastructure.swagger.AuthorsAsQueryParam +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam import org.gotson.komga.infrastructure.web.Authors @@ -119,6 +120,7 @@ class SeriesController( private val thumbnailsSeriesRepository: ThumbnailSeriesRepository, private val contentRestrictionChecker: ContentRestrictionChecker, ) { + @Operation(summary = "List series", description = "Use POST /api/v1/series/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED]) @Deprecated("use /v1/series/list instead") @PageableAsQueryParam @AuthorsAsQueryParam @@ -131,7 +133,6 @@ class SeriesController( ), ) @GetMapping("v1/series") - @Operation(summary = "Use POST /api/v1/series/list instead") fun getAllSeries( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", required = false) searchTerm: String? = null, @@ -222,6 +223,7 @@ class SeriesController( .map { it.restrictUrl(!principal.user.isAdmin) } } + @Operation(summary = "List series", tags = [OpenApiConfiguration.TagNames.SERIES]) @PageableAsQueryParam @PostMapping("v1/series/list") fun getSeriesList( @@ -252,6 +254,7 @@ class SeriesController( .map { it.restrictUrl(!principal.user.isAdmin) } } + @Operation(summary = "List series groups", description = "Use POST /api/v1/series/list/alphabetical-groups instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED]) @Deprecated("use /v1/series/list/alphabetical-groups instead") @AuthorsAsQueryParam @Parameters( @@ -263,7 +266,6 @@ class SeriesController( ), ) @GetMapping("v1/series/alphabetical-groups") - @Operation(summary = "Use POST /api/v1/series/list/alphabetical-groups instead") fun getAlphabeticalGroups( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "search", required = false) searchTerm: String?, @@ -334,13 +336,14 @@ class SeriesController( return seriesDtoRepository.countByFirstCharacter(seriesSearch, SearchContext(principal.user)) } + @Operation(summary = "List series groups", description = "List series grouped by the first character of their sort title.", tags = [OpenApiConfiguration.TagNames.SERIES]) @PostMapping("v1/series/list/alphabetical-groups") fun getSeriesListByAlphabeticalGroups( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestBody search: SeriesSearch, ): List = seriesDtoRepository.countByFirstCharacter(search, SearchContext(principal.user)) - @Operation(description = "Return recently added or updated series.") + @Operation(summary = "List latest series", description = "Return recently added or updated series.", tags = [OpenApiConfiguration.TagNames.SERIES]) @PageableWithoutSortAsQueryParam @GetMapping("v1/series/latest") fun getLatestSeries( @@ -379,7 +382,7 @@ class SeriesController( ).map { it.restrictUrl(!principal.user.isAdmin) } } - @Operation(description = "Return newly added series.") + @Operation(summary = "List new series", description = "Return newly added series.", tags = [OpenApiConfiguration.TagNames.SERIES]) @PageableWithoutSortAsQueryParam @GetMapping("v1/series/new") fun getNewSeries( @@ -418,7 +421,7 @@ class SeriesController( ).map { it.restrictUrl(!principal.user.isAdmin) } } - @Operation(description = "Return recently updated series, but not newly added ones.") + @Operation(summary = "List updated series", description = "Return recently updated series, but not newly added ones.", tags = [OpenApiConfiguration.TagNames.SERIES]) @PageableWithoutSortAsQueryParam @GetMapping("v1/series/updated") fun getUpdatedSeries( @@ -457,6 +460,7 @@ class SeriesController( ).map { it.restrictUrl(!principal.user.isAdmin) } } + @Operation(summary = "Get series details", tags = [OpenApiConfiguration.TagNames.SERIES]) @GetMapping("v1/series/{seriesId}") fun getOneSeries( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -467,6 +471,7 @@ class SeriesController( it.restrictUrl(!principal.user.isAdmin) } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) + @Operation(summary = "Get series' poster image", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["v1/series/{seriesId}/thumbnail"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getSeriesDefaultThumbnail( @@ -479,6 +484,7 @@ class SeriesController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Get series poster image", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @ApiResponse(content = [Content(schema = Schema(type = "string", format = "binary"))]) @GetMapping(value = ["v1/series/{seriesId}/thumbnails/{thumbnailId}"], produces = [MediaType.IMAGE_JPEG_VALUE]) fun getSeriesThumbnailById( @@ -492,6 +498,7 @@ class SeriesController( ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List series posters", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @GetMapping(value = ["v1/series/{seriesId}/thumbnails"], produces = [MediaType.APPLICATION_JSON_VALUE]) fun getSeriesThumbnails( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -504,6 +511,7 @@ class SeriesController( .map { it.toDto() } } + @Operation(summary = "Add series poster", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @PostMapping(value = ["v1/series/{seriesId}/thumbnails"], consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasRole('ADMIN')") fun postUserUploadedSeriesThumbnail( @@ -532,6 +540,7 @@ class SeriesController( ).toDto() } + @Operation(summary = "Mark series poster as selected", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @PutMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}/selected") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -546,6 +555,7 @@ class SeriesController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "Delete series poster", tags = [OpenApiConfiguration.TagNames.SERIES_POSTER]) @DeleteMapping("v1/series/{seriesId}/thumbnails/{thumbnailId}") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -563,11 +573,11 @@ class SeriesController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) } + @Operation(summary = "List series' books", description = "Use POST /api/v1/books/list instead. Deprecated since 1.19.0.", tags = [OpenApiConfiguration.TagNames.SERIES, OpenApiConfiguration.TagNames.DEPRECATED]) @Deprecated("use /v1/books/list instead") @PageableAsQueryParam @AuthorsAsQueryParam @GetMapping("v1/series/{seriesId}/books") - @Operation(summary = "Use POST /api/v1/books/list instead") fun getAllBooksBySeries( @AuthenticationPrincipal principal: KomgaPrincipal, @PathVariable(name = "seriesId") seriesId: String, @@ -618,6 +628,7 @@ class SeriesController( ).map { it.restrictUrl(!principal.user.isAdmin) } } + @Operation(summary = "List series' collections", tags = [OpenApiConfiguration.TagNames.SERIES]) @GetMapping("v1/series/{seriesId}/collections") fun getAllCollectionsBySeries( @AuthenticationPrincipal principal: KomgaPrincipal, @@ -630,6 +641,7 @@ class SeriesController( .map { it.toDto() } } + @Operation(summary = "Analyze series", tags = [OpenApiConfiguration.TagNames.SERIES]) @PostMapping("v1/series/{seriesId}/analyze") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -639,6 +651,7 @@ class SeriesController( taskEmitter.analyzeBook(bookRepository.findAllBySeriesId(seriesId), HIGH_PRIORITY) } + @Operation(summary = "Refresh series metadata", tags = [OpenApiConfiguration.TagNames.SERIES]) @PostMapping("v1/series/{seriesId}/metadata/refresh") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) @@ -651,6 +664,7 @@ class SeriesController( taskEmitter.refreshSeriesLocalArtwork(seriesId, priority = HIGH_PRIORITY) } + @Operation(summary = "Update series metadata", tags = [OpenApiConfiguration.TagNames.SERIES]) @PatchMapping("v1/series/{seriesId}/metadata") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.NO_CONTENT) @@ -725,7 +739,7 @@ class SeriesController( seriesRepository.findByIdOrNull(seriesId)?.let { eventPublisher.publishEvent(DomainEvent.SeriesUpdated(it)) } } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) - @Operation(description = "Mark all book for series as read") + @Operation(summary = "Mark series as read", description = "Mark all book for series as read", tags = [OpenApiConfiguration.TagNames.SERIES]) @PostMapping("v1/series/{seriesId}/read-progress") @ResponseStatus(HttpStatus.NO_CONTENT) fun markAsRead( @@ -737,7 +751,7 @@ class SeriesController( seriesLifecycle.markReadProgressCompleted(seriesId, principal.user) } - @Operation(description = "Mark all book for series as unread") + @Operation(summary = "Mark series as unread", description = "Mark all book for series as unread", tags = [OpenApiConfiguration.TagNames.SERIES]) @DeleteMapping("v1/series/{seriesId}/read-progress") @ResponseStatus(HttpStatus.NO_CONTENT) fun markAsUnread( @@ -749,6 +763,7 @@ class SeriesController( seriesLifecycle.deleteReadProgress(seriesId, principal.user) } + @Operation(summary = "Get series read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON]) @GetMapping("v2/series/{seriesId}/read-progress/tachiyomi") fun getReadProgressTachiyomiV2( @PathVariable seriesId: String, @@ -759,6 +774,7 @@ class SeriesController( return readProgressDtoRepository.findProgressV2BySeries(seriesId, principal.user.id) } + @Operation(summary = "Update series read progress (Mihon)", description = "Mihon specific, due to how read progress is handled in Mihon.", tags = [OpenApiConfiguration.TagNames.MIHON]) @PutMapping("v2/series/{seriesId}/read-progress/tachiyomi") @ResponseStatus(HttpStatus.NO_CONTENT) fun markReadProgressTachiyomiV2( @@ -781,6 +797,7 @@ class SeriesController( } } + @Operation(summary = "Download series", description = "Download the whole series as a ZIP file.", tags = [OpenApiConfiguration.TagNames.SERIES]) @GetMapping("v1/series/{seriesId}/file", produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE]) @PreAuthorize("hasRole('FILE_DOWNLOAD')") fun getSeriesFile( @@ -828,6 +845,7 @@ class SeriesController( .body(streamingResponse) } + @Operation(summary = "Delete series files", description = "Delete all of the series' books files on disk.", tags = [OpenApiConfiguration.TagNames.SERIES]) @DeleteMapping("v1/series/{seriesId}/file") @PreAuthorize("hasRole('ADMIN')") @ResponseStatus(HttpStatus.ACCEPTED) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SettingsController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SettingsController.kt index 4ae30d729..3ce967de2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SettingsController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SettingsController.kt @@ -1,8 +1,12 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.tags.Tag import jakarta.validation.Valid import org.gotson.komga.infrastructure.configuration.KomgaSettingsProvider import org.gotson.komga.infrastructure.kobo.KepubConverter +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.web.WebServerEffectiveSettings import org.gotson.komga.interfaces.api.rest.dto.SettingMultiSource import org.gotson.komga.interfaces.api.rest.dto.SettingsDto @@ -24,6 +28,7 @@ import kotlin.time.Duration.Companion.days @RestController @RequestMapping(value = ["api/v1/settings"], produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("hasRole('ADMIN')") +@Tag(name = OpenApiConfiguration.TagNames.SERVER_SETTINGS) class SettingsController( private val komgaSettingsProvider: KomgaSettingsProvider, @Value("\${server.port:#{null}}") private val configServerPort: Int?, @@ -32,6 +37,7 @@ class SettingsController( private val kepubConverter: KepubConverter, ) { @GetMapping + @Operation(summary = "Retrieve server settings") fun getSettings(): SettingsDto = SettingsDto( komgaSettingsProvider.deleteEmptyCollections, @@ -48,8 +54,10 @@ class SettingsController( @PatchMapping @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Update server settings", description = "You can omit fields you don't want to update") fun updateSettings( @Valid @RequestBody + @Parameter(description = "Fields to update. You can omit fields you don't want to update.") newSettings: SettingsUpdateDto, ) { newSettings.deleteEmptyCollections?.let { komgaSettingsProvider.deleteEmptyCollections = it } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SyncPointController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SyncPointController.kt index 7f9c002ae..a46665d46 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SyncPointController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SyncPointController.kt @@ -1,7 +1,10 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.domain.persistence.SyncPointRepository import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -13,11 +16,16 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("api/v1/syncpoints", produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.SYNCPOINTS) class SyncPointController( private val syncPointRepository: SyncPointRepository, ) { @DeleteMapping("me") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation( + summary = "Delete all sync points", + description = "If an API Key ID is passed, deletes only the sync points associated with that API Key. Deleting sync points will allow a Kobo to sync from scratch upon the next sync.", + ) fun deleteMySyncPointsByApiKey( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(required = false, name = "key_id") keyIds: Collection?, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TaskController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TaskController.kt index c1dc4562b..f46ed6653 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TaskController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TaskController.kt @@ -1,6 +1,9 @@ package org.gotson.komga.interfaces.api.rest +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.application.tasks.TasksRepository +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.security.access.prepost.PreAuthorize @@ -11,11 +14,13 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE]) +@Tag(name = OpenApiConfiguration.TagNames.TASKS) class TaskController( private val tasksRepository: TasksRepository, ) { @DeleteMapping("api/v1/tasks") @ResponseStatus(HttpStatus.OK) @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Clear task queue", description = "Cancel all tasks queued") fun emptyTaskQueue(): Int = tasksRepository.deleteAllWithoutOwner() } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt index 43116ef74..b71db82b8 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/TransientBooksController.kt @@ -2,6 +2,8 @@ package org.gotson.komga.interfaces.api.rest import com.jakewharton.byteunits.BinaryByteUnit import io.github.oshai.kotlinlogging.KotlinLogging +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag import org.gotson.komga.domain.model.CodedException import org.gotson.komga.domain.model.MediaNotReadyException import org.gotson.komga.domain.model.MediaProfile @@ -9,6 +11,7 @@ import org.gotson.komga.domain.model.TransientBook import org.gotson.komga.domain.persistence.TransientBookRepository import org.gotson.komga.domain.service.BookAnalyzer import org.gotson.komga.domain.service.TransientBookLifecycle +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault import org.gotson.komga.infrastructure.web.toFilePath import org.gotson.komga.interfaces.api.rest.dto.PageDto @@ -31,12 +34,14 @@ private val logger = KotlinLogging.logger {} @RestController @RequestMapping("api/v1/transient-books", produces = [MediaType.APPLICATION_JSON_VALUE]) @PreAuthorize("hasRole('ADMIN')") +@Tag(name = OpenApiConfiguration.TagNames.BOOK_IMPORT) class TransientBooksController( private val transientBookLifecycle: TransientBookLifecycle, private val transientBookRepository: TransientBookRepository, private val bookAnalyzer: BookAnalyzer, ) { @PostMapping + @Operation(summary = "Scan folder for transient books", description = "Scan provided folder for transient books.") fun scanForTransientBooks( @RequestBody request: ScanRequestDto, ): List = @@ -50,6 +55,7 @@ class TransientBooksController( } @PostMapping("{id}/analyze") + @Operation(summary = "Analyze transient book") fun analyze( @PathVariable id: String, ): TransientBookDto = @@ -61,6 +67,7 @@ class TransientBooksController( value = ["{id}/pages/{pageNumber}"], produces = [MediaType.ALL_VALUE], ) + @Operation(summary = "Get transient book page") fun getSourcePage( @PathVariable id: String, @PathVariable pageNumber: Int, diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/UserController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/UserController.kt index 929ea84fc..43781bbb6 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/UserController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/UserController.kt @@ -1,6 +1,7 @@ package org.gotson.komga.interfaces.api.rest import io.github.oshai.kotlinlogging.KotlinLogging +import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.Parameter import jakarta.validation.Valid import org.gotson.komga.domain.model.AgeRestriction @@ -14,6 +15,7 @@ import org.gotson.komga.domain.persistence.LibraryRepository import org.gotson.komga.domain.service.KomgaUserLifecycle import org.gotson.komga.infrastructure.jooq.UnpagedSorted import org.gotson.komga.infrastructure.security.KomgaPrincipal +import org.gotson.komga.infrastructure.swagger.OpenApiConfiguration.TagNames import org.gotson.komga.interfaces.api.rest.dto.ApiKeyDto import org.gotson.komga.interfaces.api.rest.dto.ApiKeyRequestDto import org.gotson.komga.interfaces.api.rest.dto.AuthenticationActivityDto @@ -59,12 +61,14 @@ class UserController( private val demo = env.activeProfiles.contains("demo") @GetMapping("me") + @Operation(summary = "Retrieve current user", tags = [TagNames.CURRENT_USER]) fun getMe( @AuthenticationPrincipal principal: KomgaPrincipal, ): UserDto = principal.toDto() @PatchMapping("me/password") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Update current user's password", tags = [TagNames.CURRENT_USER]) fun updateMyPassword( @AuthenticationPrincipal principal: KomgaPrincipal, @Valid @RequestBody @@ -78,11 +82,13 @@ class UserController( @GetMapping @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "List users", tags = [TagNames.USERS]) fun getAll(): List = userRepository.findAll().map { it.toDto() } @PostMapping @ResponseStatus(HttpStatus.CREATED) @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Create user", tags = [TagNames.USERS]) fun addOne( @Valid @RequestBody newUser: UserCreationDto, @@ -96,6 +102,7 @@ class UserController( @DeleteMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasRole('ADMIN') and #principal.user.id != #id") + @Operation(summary = "Delete user", tags = [TagNames.USERS]) fun delete( @PathVariable id: String, @AuthenticationPrincipal principal: KomgaPrincipal, @@ -108,6 +115,7 @@ class UserController( @PatchMapping("{id}") @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasRole('ADMIN') and #principal.user.id != #id") + @Operation(summary = "Update user", tags = [TagNames.USERS]) fun updateUser( @PathVariable id: String, @Valid @RequestBody @@ -162,6 +170,7 @@ class UserController( @PatchMapping("{id}/password") @ResponseStatus(HttpStatus.NO_CONTENT) @PreAuthorize("hasRole('ADMIN') or #principal.user.id == #id") + @Operation(summary = "Update user's password", tags = [TagNames.USERS]) fun updatePassword( @PathVariable id: String, @AuthenticationPrincipal principal: KomgaPrincipal, @@ -176,6 +185,7 @@ class UserController( @GetMapping("me/authentication-activity") @PageableAsQueryParam + @Operation(summary = "Retrieve authentication activity for the current user", tags = [TagNames.CURRENT_USER]) fun getMyAuthenticationActivity( @AuthenticationPrincipal principal: KomgaPrincipal, @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, @@ -204,6 +214,7 @@ class UserController( @GetMapping("authentication-activity") @PageableAsQueryParam @PreAuthorize("hasRole('ADMIN')") + @Operation(summary = "Retrieve authentication activity", tags = [TagNames.USERS]) fun getAuthenticationActivity( @RequestParam(name = "unpaged", required = false) unpaged: Boolean = false, @Parameter(hidden = true) page: Pageable, @@ -229,6 +240,7 @@ class UserController( @GetMapping("{id}/authentication-activity/latest") @PreAuthorize("hasRole('ADMIN') or #principal.user.id == #id") + @Operation(summary = "Retrieve latest authentication activity for a user", tags = [TagNames.USERS]) fun getLatestAuthenticationActivityForUser( @PathVariable id: String, @AuthenticationPrincipal principal: KomgaPrincipal, @@ -240,6 +252,7 @@ class UserController( } ?: throw ResponseStatusException(HttpStatus.NOT_FOUND) @GetMapping("me/api-keys") + @Operation(summary = "Retrieve API keys", tags = [TagNames.API_KEYS]) fun getApiKeys( @AuthenticationPrincipal principal: KomgaPrincipal, ): Collection { @@ -248,6 +261,7 @@ class UserController( } @PostMapping("me/api-keys") + @Operation(summary = "Create API key", tags = [TagNames.API_KEYS]) fun createApiKey( @AuthenticationPrincipal principal: KomgaPrincipal, @Valid @RequestBody apiKeyRequest: ApiKeyRequestDto, @@ -263,6 +277,7 @@ class UserController( @DeleteMapping("me/api-keys/{keyId}") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete API key", tags = [TagNames.API_KEYS]) fun deleteApiKey( @AuthenticationPrincipal principal: KomgaPrincipal, @PathVariable keyId: String, diff --git a/komga/src/main/resources/application.yml b/komga/src/main/resources/application.yml index e7c508215..33a1f79a1 100644 --- a/komga/src/main/resources/application.yml +++ b/komga/src/main/resources/application.yml @@ -94,8 +94,6 @@ management: step: 24h springdoc: swagger-ui: - groups-order: desc - operations-sorter: alpha disable-swagger-default-url: true - tags-sorter: alpha paths-to-match: "/api/**" + writer-with-order-by-keys: true