diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4200110ea..7435b38a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,6 +182,8 @@ Create a dev config at `booklore-api/src/main/resources/application-dev.yml`: app: path-book: '/path/to/booklore-data/books' path-config: '/path/to/booklore-data/config' + api-docs: + enabled: true spring: datasource: @@ -211,6 +213,14 @@ The UI will be available at http://localhost:4200 with hot-reload enabled. --- +## API Reference Docs + +When enabled, API documentation is available as both an `openapi.json` and as publicly accessible docs. The endpoints are: +- Scalar UI is available at `http://localhost:6060/api/docs` +- OpenAPI JSON is available at `http://localhost:6060/api/openapi.json` + +--- + ## Running Tests Always run tests before submitting a pull request. diff --git a/README.md b/README.md index 89bdef0cd..c3df91ee7 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,9 @@ DATABASE_URL=jdbc:mariadb://mariadb:3306/grimmory DB_USER=grimmory DB_PASSWORD=ChangeMe_Grimmory_2025! +# Optional: enable API docs + export OpenAPI JSON (defaults to false) +API_DOCS_ENABLED=false + # Storage: LOCAL (default) or NETWORK (disables file operations; see Network Storage section) DISK_TYPE=LOCAL @@ -117,6 +120,7 @@ services: - DATABASE_URL=${DATABASE_URL} - DATABASE_USERNAME=${DB_USER} - DATABASE_PASSWORD=${DB_PASSWORD} + - API_DOCS_ENABLED=${API_DOCS_ENABLED} depends_on: mariadb: condition: service_healthy @@ -194,6 +198,14 @@ just ui dev # Start the frontend dev server --- +## API Reference Docs + +When enabled, API reference documentation is available as both an `openapi.json` and as publicly accessible docs. The endpoints are: +- API reference docs are available at `http://localhost:6060/api/docs` +- OpenAPI JSON is available at `http://localhost:6060/api/openapi.json` + +--- + ## BookDrop Drop book files into a watched folder. Grimmory picks them up, pulls metadata, and queues them for your review. diff --git a/booklore-api/CONTRIBUTING.md b/booklore-api/CONTRIBUTING.md index c49567ab7..0562919c3 100644 --- a/booklore-api/CONTRIBUTING.md +++ b/booklore-api/CONTRIBUTING.md @@ -24,6 +24,15 @@ just api run just api check ``` +## API Docs (`API_DOCS_ENABLED`) + +- `application.yaml` binds `app.api-docs.enabled` to `API_DOCS_ENABLED` (default `false`). +- `application.yaml` binds `springdoc.api-docs.enabled` to `app.api-docs.enabled`. +- `application-dev.yml` enables docs for local dev profile runs by default. +- For runtime/container profiles, set `API_DOCS_ENABLED=true` to expose: + - `/api/openapi.json` + - `/api/docs` + ## Backend Conventions - Use Spring Data JPA repository methods or JPQL. Do not introduce native queries unless a maintainer has explicitly approved them. diff --git a/booklore-api/build.gradle.kts b/booklore-api/build.gradle.kts index 050932239..ed2c39335 100644 --- a/booklore-api/build.gradle.kts +++ b/booklore-api/build.gradle.kts @@ -97,7 +97,7 @@ dependencies { annotationProcessor("org.mapstruct:mapstruct-processor:1.6.3") // --- API Documentation --- - implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.2") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.2") implementation("org.apache.commons:commons-compress:1.28.0") implementation("org.tukaani:xz:1.12") // Required by commons-compress for 7z support implementation("org.apache.commons:commons-text:1.15.0") diff --git a/booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java b/booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java index 2f61a2e14..04c184957 100644 --- a/booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java +++ b/booklore-api/src/main/java/org/booklore/config/security/SecurityConfig.java @@ -50,6 +50,8 @@ public class SecurityConfig { private static final String[] COMMON_PUBLIC_ENDPOINTS = { "/ws/**", // WebSocket connections (auth handled in WebSocketAuthInterceptor) "/kobo/**", // Kobo API requests (auth handled in KoboAuthFilter) + "/api/docs", // API documentation UI + "/api/openapi.json", // OpenAPI spec (used by API documentation UI) "/api/v1/auth/**", // Login and token refresh endpoints (must remain public) "/api/v1/public-settings", // Public endpoint for checking OIDC or other app settings "/api/v1/setup/**", // Setup wizard endpoints (must remain accessible before initial setup) diff --git a/booklore-api/src/main/java/org/booklore/controller/ScalarController.java b/booklore-api/src/main/java/org/booklore/controller/ScalarController.java new file mode 100644 index 000000000..42f34e2e8 --- /dev/null +++ b/booklore-api/src/main/java/org/booklore/controller/ScalarController.java @@ -0,0 +1,15 @@ +package org.booklore.controller; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +@ConditionalOnProperty(name = "app.api-docs.enabled", havingValue = "true", matchIfMissing = false) +public class ScalarController { + + @GetMapping("/api/docs") + public String scalar() { + return "forward:/scalar.html"; + } +} diff --git a/booklore-api/src/main/resources/application.yaml b/booklore-api/src/main/resources/application.yaml index 01d4810f2..24958dd77 100644 --- a/booklore-api/src/main/resources/application.yaml +++ b/booklore-api/src/main/resources/application.yaml @@ -2,6 +2,8 @@ app: path-config: '/app/data' bookdrop-folder: '/bookdrop' version: ${APP_VERSION:development} + api-docs: + enabled: ${API_DOCS_ENABLED:false} cors: # Comma-separated list of allowed CORS origins. @@ -93,9 +95,8 @@ spring: springdoc: api-docs: - enabled: false - swagger-ui: - enabled: false + enabled: ${app.api-docs.enabled:false} + path: /api/openapi.json logging: level: diff --git a/booklore-api/src/main/resources/static/scalar.html b/booklore-api/src/main/resources/static/scalar.html new file mode 100644 index 000000000..c01b25575 --- /dev/null +++ b/booklore-api/src/main/resources/static/scalar.html @@ -0,0 +1,19 @@ + + + + Grimmory API Reference + + + + + + + + diff --git a/booklore-api/src/test/java/org/booklore/controller/ScalarControllerTest.java b/booklore-api/src/test/java/org/booklore/controller/ScalarControllerTest.java new file mode 100644 index 000000000..d82ae6c92 --- /dev/null +++ b/booklore-api/src/test/java/org/booklore/controller/ScalarControllerTest.java @@ -0,0 +1,30 @@ +package org.booklore.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.util.StreamUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class ScalarControllerTest { + + private final ScalarController controller = new ScalarController(); + + @Test + void shouldForwardToStaticScalarPage() { + assertThat(controller.scalar()).isEqualTo("forward:/scalar.html"); + } + + @Test + void staticScalarPageShouldDisableTelemetryAndPointToApiDocs() throws IOException { + String html = StreamUtils.copyToString( + new ClassPathResource("static/scalar.html").getInputStream(), + StandardCharsets.UTF_8 + ); + assertThat(html).contains("data-url=\"/api/openapi.json\""); + assertThat(html).contains("\"telemetry\":false"); + } +}