Commit 11bef932eec9e43cc17efef45577099e833b568c

Authored by zichun
1 parent f01ba0d7

feat(bootstrap+security): OpenAPI 3 spec + Swagger UI

Adds self-introspection of the framework's REST surface via
springdoc-openapi. Every @RestController method in the host
application is now documented in a machine-readable OpenAPI 3
spec at /v3/api-docs and rendered for humans at
/swagger-ui/index.html. This is the first step toward:

  - R1 (web SPA): OpenAPI codegen feeds a typed TypeScript client
  - A1 (MCP server): discoverable tool catalog
  - Operator debugging: browsable "what can this instance do" page

**Dependency.** New `springdoc-openapi-starter-webmvc-ui` 2.6.0
added to platform-bootstrap (not distribution) because it ships
@Configuration classes that need to run inside a full Spring Boot
application context AND brings a Swagger UI WebJar. platform-bootstrap
is the only module with a @SpringBootApplication anyway; pbc
modules never depend on it, so plug-in classloaders stay clean
and the OpenAPI scanner only sees host controllers.

**Configuration.** New `OpenApiConfiguration` @Configuration in
platform-bootstrap provides a single @Bean OpenAPI:
  - Title "vibe_erp", version v0.28.0 (hardcoded; moves to a
    build property when a real version header ships)
  - Description with a framework-level intro explaining the
    bearer-JWT auth model, the permission whitelist, and the
    fact that plug-in endpoints under /api/v1/plugins/{id}/** are
    NOT scanned (they are dynamically registered via
    PluginContext.endpoints on a single dispatcher controller;
    a future chunk may extend the spec at runtime).
  - One relative server entry ("/") so the spec works behind a
    reverse proxy without baking localhost into it.
  - bearerAuth security scheme (HTTP/bearer/JWT) applied globally
    via addSecurityItem, so every operation in the rendered UI
    shows a lock icon and the "Authorize" button accepts a raw
    JWT (Swagger adds the "Bearer " prefix itself).

**Security whitelist.** SecurityConfiguration now permits three
additional path patterns without authentication:
  - /v3/api-docs/** — the generated JSON spec
  - /swagger-ui/** — the Swagger UI static assets + index
  - /swagger-ui.html — the legacy path (redirects to the above)
The data still requires a valid JWT: an unauthenticated "Try it
out" call from the Swagger UI against a pbc endpoint returns 401
exactly like a curl would.

**Why not wire this into every PBC controller with @Operation /
@Parameter annotations in this chunk:** springdoc already
auto-generates the full path + request body + response schema
from reflection. Adding hand-written annotations is scope creep —
a future chunk can tag per-operation @Operation(security = ...)
to surface the @RequirePermission keys once a consumer actually
needs them.

**Smoke-tested end-to-end against real Postgres:**
  - GET /v3/api-docs returns 200 with 64680 bytes of OpenAPI JSON
  - 76 total paths listed across every PBC controller
  - All v3 production paths present: /work-orders/shop-floor,
    /work-orders/{id}/operations/{operationId}/start + /complete,
    /work-orders/{id}/{start,complete,cancel,scrap}
  - components.securitySchemes includes bearerAuth (type=http,
    format=JWT)
  - GET /swagger-ui/index.html returns 200 with the Swagger HTML
    bundle (5 swagger markers found in the HTML)
  - GET /swagger-ui.html (legacy path) returns 200 after redirect

25 modules (unchanged count — new config lives inside
platform-bootstrap), 355 unit tests, all green.
gradle/libs.versions.toml
... ... @@ -9,6 +9,7 @@ liquibase = "4.29.2"
9 9 pf4j = "3.12.0"
10 10 flowable = "7.0.1"
11 11 jasperreports = "6.21.3"
  12 +springdoc = "2.6.0"
12 13 icu4j = "75.1"
13 14 jackson = "2.18.0"
14 15 junitJupiter = "5.11.2"
... ... @@ -61,6 +62,9 @@ spring-boot-starter-quartz = { module = "org.springframework.boot:spring-boot-st
61 62 # Reports (JasperReports PDF rendering)
62 63 jasperreports = { module = "net.sf.jasperreports:jasperreports", version.ref = "jasperreports" }
63 64  
  65 +# OpenAPI / Swagger UI (runtime introspection of @RestController endpoints)
  66 +springdoc-openapi-starter-webmvc-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
  67 +
64 68 # i18n
65 69 icu4j = { module = "com.ibm.icu:icu4j", version.ref = "icu4j" }
66 70  
... ...
platform/platform-bootstrap/build.gradle.kts
... ... @@ -32,6 +32,14 @@ dependencies {
32 32 implementation(libs.spring.boot.starter.data.jpa) // for @EnableJpaRepositories on VibeErpApplication
33 33 implementation(libs.spring.boot.starter.validation)
34 34 implementation(libs.spring.boot.starter.actuator)
  35 + // springdoc-openapi produces /v3/api-docs (the OpenAPI 3 JSON) and
  36 + // /swagger-ui/index.html from the @RestController classes it scans
  37 + // at runtime. Lives here alongside MetaController because it's part
  38 + // of the same "framework self-introspection" story that MetaController
  39 + // already serves (/_meta/info, /_meta/metadata). PBC modules never
  40 + // depend on platform-bootstrap so no @RestController leaks into a
  41 + // plug-in classloader; the scan only sees host controllers.
  42 + implementation(libs.springdoc.openapi.starter.webmvc.ui)
35 43  
36 44 testImplementation(libs.spring.boot.starter.test)
37 45 testImplementation(libs.junit.jupiter)
... ...
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt 0 → 100644
  1 +package org.vibeerp.platform.bootstrap.openapi
  2 +
  3 +import io.swagger.v3.oas.models.Components
  4 +import io.swagger.v3.oas.models.OpenAPI
  5 +import io.swagger.v3.oas.models.info.Contact
  6 +import io.swagger.v3.oas.models.info.Info
  7 +import io.swagger.v3.oas.models.info.License
  8 +import io.swagger.v3.oas.models.security.SecurityRequirement
  9 +import io.swagger.v3.oas.models.security.SecurityScheme
  10 +import io.swagger.v3.oas.models.servers.Server
  11 +import org.springframework.context.annotation.Bean
  12 +import org.springframework.context.annotation.Configuration
  13 +
  14 +/**
  15 + * Provides the top-level [OpenAPI] bean that `springdoc-openapi`
  16 + * merges into the auto-generated spec it serves at `/v3/api-docs`.
  17 + *
  18 + * **Why a @Configuration instead of just letting springdoc
  19 + * default-generate:** the default bean has no meta (title is the
  20 + * jar name, description is empty, no security scheme). Filling in
  21 + * framework identity + the bearer-JWT scheme is what makes the
  22 + * Swagger UI's "Authorize" button work out of the box and what
  23 + * lets downstream OpenAPI clients (R1 web SPA codegen, A1 MCP
  24 + * server tool catalog) pick up the bearer requirement
  25 + * declaratively instead of having every generated client paste in
  26 + * a hand-coded Authorization header.
  27 + *
  28 + * **Bearer JWT scheme, declared once.**
  29 + * - A `bearerAuth` security scheme is registered in [Components],
  30 + * matching the JWT format the framework already issues via
  31 + * `/api/v1/auth/login` and validates via `spring-boot-starter-oauth2-resource-server`.
  32 + * - It's applied globally as a default [SecurityRequirement], so
  33 + * every listed operation shows a lock icon unless it's on the
  34 + * explicit whitelist (actuator health, auth endpoints, `_meta`,
  35 + * `v3/api-docs`, `swagger-ui`).
  36 + * - Controllers that want to explicitly opt-out can still use
  37 + * `@SecurityRequirements()` (empty) on a method.
  38 + *
  39 + * **Single relative server entry.** The OpenAPI spec lists ONE
  40 + * server entry: the current host, relative path. This avoids
  41 + * baking localhost:8080 into the spec when the app is deployed
  42 + * behind a reverse proxy. A future multi-environment story can
  43 + * grow this list from configuration.
  44 + *
  45 + * **Version source.** [OPENAPI_INFO_VERSION] is a build-time
  46 + * constant kept in sync with the framework's `PROGRESS.md` at
  47 + * memory time; it's not parsed out of any manifest because the
  48 + * distribution fat-jar doesn't carry a semver yet. When a real
  49 + * version header lands this moves to a @Value on a config
  50 + * property, but hardcoding it now keeps the chunk small.
  51 + */
  52 +@Configuration
  53 +class OpenApiConfiguration {
  54 +
  55 + @Bean
  56 + fun vibeErpOpenApi(): OpenAPI {
  57 + val bearerScheme = SecurityScheme()
  58 + .type(SecurityScheme.Type.HTTP)
  59 + .scheme("bearer")
  60 + .bearerFormat("JWT")
  61 + .description(
  62 + "JWT access token obtained from POST /api/v1/auth/login. " +
  63 + "Paste the token without the 'Bearer ' prefix — Swagger UI adds it.",
  64 + )
  65 +
  66 + return OpenAPI()
  67 + .info(
  68 + Info()
  69 + .title("vibe_erp")
  70 + .version(OPENAPI_INFO_VERSION)
  71 + .description(
  72 + """
  73 + Auto-generated OpenAPI 3 spec for the vibe_erp framework core and every
  74 + loaded plug-in's HTTP surface.
  75 +
  76 + **Auth.** Every endpoint except the handful whitelisted in `SecurityConfiguration`
  77 + (actuator health, `/api/v1/auth/login`, `/api/v1/auth/refresh`, `/api/v1/_meta/**`,
  78 + `/v3/api-docs/**`, `/swagger-ui/**`) requires a Bearer JWT. Obtain one by
  79 + POSTing `{ "username": "...", "password": "..." }` to `/api/v1/auth/login`.
  80 +
  81 + **Permissions.** Controller methods are annotated with `@RequirePermission("...")`;
  82 + the spec does NOT currently surface those keys into the OpenAPI `security`
  83 + array — a future chunk can extend the controllers with `@Operation(security = ...)`
  84 + so generated clients can introspect per-operation permissions.
  85 +
  86 + **Plug-in endpoints.** Endpoints mounted by plug-ins under `/api/v1/plugins/{id}/**`
  87 + are NOT auto-discovered by this scan — they are registered dynamically at plug-in
  88 + start time through `PluginContext.endpoints` and live on a single dispatcher
  89 + `@RestController`. A future chunk may extend the spec at runtime as plug-ins
  90 + load/unload.
  91 + """.trimIndent(),
  92 + )
  93 + .contact(
  94 + Contact()
  95 + .name("vibe_erp")
  96 + .url("https://github.com/reporkey/vibe-erp"),
  97 + )
  98 + .license(
  99 + License()
  100 + .name("Proprietary")
  101 + .url("https://github.com/reporkey/vibe-erp"),
  102 + ),
  103 + )
  104 + .servers(
  105 + listOf(
  106 + Server()
  107 + .url("/")
  108 + .description("Current host (relative — works behind a reverse proxy)"),
  109 + ),
  110 + )
  111 + .components(
  112 + Components()
  113 + .addSecuritySchemes(BEARER_SCHEME_NAME, bearerScheme),
  114 + )
  115 + .addSecurityItem(
  116 + SecurityRequirement().addList(BEARER_SCHEME_NAME),
  117 + )
  118 + }
  119 +
  120 + companion object {
  121 + /**
  122 + * Name of the security scheme registered in Components.
  123 + * Referenced from [addSecurityItem] for the global default
  124 + * and from `@SecurityRequirement(name = ...)` on any
  125 + * controller method that wants per-operation overrides.
  126 + */
  127 + const val BEARER_SCHEME_NAME: String = "bearerAuth"
  128 +
  129 + /**
  130 + * Displayed in Swagger UI's header. Hardcoded for now; when
  131 + * a real build-time version header ships this becomes a
  132 + * configuration property.
  133 + */
  134 + const val OPENAPI_INFO_VERSION: String = "v0.28.0"
  135 + }
  136 +}
... ...
platform/platform-security/src/main/kotlin/org/vibeerp/platform/security/SecurityConfiguration.kt
... ... @@ -46,6 +46,16 @@ class SecurityConfiguration {
46 46 // Auth endpoints themselves cannot require auth
47 47 "/api/v1/auth/login",
48 48 "/api/v1/auth/refresh",
  49 + // OpenAPI / Swagger UI — public because it describes the
  50 + // API surface, not the data. The *data* still requires a
  51 + // valid JWT: an unauthenticated "Try it out" call from
  52 + // the Swagger UI against a pbc endpoint returns 401 just
  53 + // like a curl does. Exposing the spec is the first step
  54 + // toward both the future R1 web SPA (OpenAPI codegen)
  55 + // and the A1 MCP server (discoverable tool catalog).
  56 + "/v3/api-docs/**",
  57 + "/swagger-ui/**",
  58 + "/swagger-ui.html",
49 59 ).permitAll()
50 60 auth.anyRequest().authenticated()
51 61 }
... ...