From 0e51c8f287f331bbf760329c5325623673c11a44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Fri, 10 Oct 2025 14:34:30 +0200 Subject: [PATCH 1/2] feat: Prepare V3 API --- .../org/knora/webapi/core/LayersLive.scala | 3 + .../org/knora/webapi/routing/Endpoints.scala | 19 ++--- .../webapi/slice/api/v3/ApiV3Module.scala | 23 ++++++ .../slice/api/v3/ApiV3ServerEndpoints.scala | 17 +++++ .../webapi/slice/api/v3/V3BaseEndpoint.scala | 70 +++++++++++++++++++ .../webapi/slice/api/v3/V3ErrorCode.scala | 17 +++++ .../webapi/slice/api/v3/V3ErrorInfo.scala | 52 ++++++++++++++ .../slice/infrastructure/MetricsServer.scala | 39 ++++------- .../webapi/slice/api/v3/NotFoundSpec.scala | 36 ++++++++++ 9 files changed, 243 insertions(+), 33 deletions(-) create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3Module.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3ServerEndpoints.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3BaseEndpoint.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorCode.scala create mode 100644 webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorInfo.scala create mode 100644 webapi/src/test/scala/org/knora/webapi/slice/api/v3/NotFoundSpec.scala diff --git a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala index 4c2c6a0cb8..b05b483b31 100644 --- a/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala +++ b/webapi/src/main/scala/org/knora/webapi/core/LayersLive.scala @@ -32,6 +32,8 @@ import org.knora.webapi.slice.admin.AdminModule import org.knora.webapi.slice.admin.api.* import org.knora.webapi.slice.admin.domain.service.* import org.knora.webapi.slice.api.v2.ApiV2ServerEndpoints +import org.knora.webapi.slice.api.v3.ApiV3Module +import org.knora.webapi.slice.api.v3.ApiV3ServerEndpoints import org.knora.webapi.slice.common.ApiComplexV2JsonLdRequestParser import org.knora.webapi.slice.common.CommonModule import org.knora.webapi.slice.common.CommonModule.Provided @@ -136,6 +138,7 @@ object LayersLive { self => ApiComplexV2JsonLdRequestParser.layer, ApiV2Endpoints.layer, ApiV2ServerEndpoints.layer, + ApiV3Module.layer, AssetPermissionsResponder.layer, AuthenticationApiModule.layer, AuthorizationRestService.layer, diff --git a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala index ed4a9f00dd..1248976752 100644 --- a/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala +++ b/webapi/src/main/scala/org/knora/webapi/routing/Endpoints.scala @@ -12,20 +12,23 @@ import zio.* import org.knora.webapi.routing import org.knora.webapi.slice.admin.api.AdminApiServerEndpoints import org.knora.webapi.slice.api.v2.ApiV2ServerEndpoints +import org.knora.webapi.slice.api.v3.ApiV3ServerEndpoints import org.knora.webapi.slice.infrastructure.api.ManagementServerEndpoints import org.knora.webapi.slice.shacl.api.ShaclServerEndpoints final case class Endpoints( - adminApiServerEndpoints: AdminApiServerEndpoints, // admin api - apiV2ServerEndpoints: ApiV2ServerEndpoints, - shaclServerEndpoints: ShaclServerEndpoints, - managementServerEndpoints: ManagementServerEndpoints, + private val adminApi: AdminApiServerEndpoints, // admin api + private val apiV2: ApiV2ServerEndpoints, + private val apiV3: ApiV3ServerEndpoints, + private val shacl: ShaclServerEndpoints, + private val management: ManagementServerEndpoints, ) { val serverEndpoints: List[ZServerEndpoint[Any, ZioStreams]] = - adminApiServerEndpoints.serverEndpoints ++ - apiV2ServerEndpoints.serverEndpoints ++ - managementServerEndpoints.serverEndpoints ++ - shaclServerEndpoints.serverEndpoints + adminApi.serverEndpoints ++ + apiV2.serverEndpoints ++ + apiV3.serverEndpoints ++ + management.serverEndpoints ++ + shacl.serverEndpoints } object Endpoints { val layer = ZLayer.derive[Endpoints] diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3Module.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3Module.scala new file mode 100644 index 0000000000..53e74390b4 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3Module.scala @@ -0,0 +1,23 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 + +import zio.* + +import org.knora.webapi.slice.security.Authenticator + +object ApiV3Module { + + type Dependencies = Authenticator + type Provided = ApiV3ServerEndpoints + + val layer: URLayer[Dependencies, ApiV3ServerEndpoints] = + V3BaseEndpoint.layer >>> ApiV3ServerEndpoints.layer +} + +object ApiV3 { + val basePath = "v3" +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3ServerEndpoints.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3ServerEndpoints.scala new file mode 100644 index 0000000000..25c9f70568 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/ApiV3ServerEndpoints.scala @@ -0,0 +1,17 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 + +import sttp.tapir.ztapir.* +import zio.* + +final case class ApiV3ServerEndpoints() { + val serverEndpoints: List[ZServerEndpoint[Any, Any]] = List.empty +} + +object ApiV3ServerEndpoints { + val layer = ZLayer.derive[ApiV3ServerEndpoints] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3BaseEndpoint.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3BaseEndpoint.scala new file mode 100644 index 0000000000..d16bc26a0a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3BaseEndpoint.scala @@ -0,0 +1,70 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 + +import sttp.model.StatusCode +import sttp.model.headers.WWWAuthenticateChallenge +import sttp.tapir.EndpointOutput +import sttp.tapir.PublicEndpoint +import sttp.tapir.Validator +import sttp.tapir.generic.auto.* +import sttp.tapir.json.zio.jsonBody +import sttp.tapir.ztapir.* +import zio.* +import zio.json.JsonEncoder + +import org.knora.webapi.messages.util.KnoraSystemInstances.Users.AnonymousUser +import org.knora.webapi.slice.admin.domain.model.* +import org.knora.webapi.slice.api.v3.V3ErrorInfo.* +import org.knora.webapi.slice.security.Authenticator +import org.knora.webapi.slice.security.AuthenticatorError.* + +final case class V3BaseEndpoint(private val authenticator: Authenticator) { + + private val defaultErrorOut: EndpointOutput.OneOf[V3ErrorInfo, V3ErrorInfo] = + oneOf[V3ErrorInfo]( + // default + oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])), + oneOfVariant(statusCode(StatusCode.BadRequest).and(jsonBody[BadRequest])), + ) + + private val secureErrorOut = oneOf[V3ErrorInfo]( + // default + oneOfVariant(statusCode(StatusCode.NotFound).and(jsonBody[NotFound])), + oneOfVariant(statusCode(StatusCode.BadRequest).and(jsonBody[BadRequest])), + // plus security + oneOfVariant(statusCode(StatusCode.Unauthorized).and(jsonBody[Unauthorized])), + oneOfVariant(statusCode(StatusCode.Forbidden).and(jsonBody[Forbidden])), + ) + + val publicEndpoint: PublicEndpoint[Unit, V3ErrorInfo, Unit, Any] = endpoint.errorOut(defaultErrorOut) + + private val endpointWithSecureErrorOut = endpoint.errorOut(secureErrorOut) + + val securedEndpoint: ZPartialServerEndpoint[Any, String, User, Unit, V3ErrorInfo, Unit, Any] = + endpointWithSecureErrorOut + .securityIn(auth.bearer[String](WWWAuthenticateChallenge.bearer)) + .zServerSecurityLogic(handleBearerJwt) + + val withUserEndpoint: ZPartialServerEndpoint[Any, Option[String], User, Unit, V3ErrorInfo, Unit, Any] = + endpointWithSecureErrorOut + .securityIn(auth.bearer[Option[String]](WWWAuthenticateChallenge.bearer)) + .zServerSecurityLogic { + case Some(jwt) => handleBearerJwt(jwt) + case _ => ZIO.succeed(AnonymousUser) + } + + private def handleBearerJwt(jwt: String): IO[V3ErrorInfo, User] = + authenticator.authenticate(jwt).mapError { + case BadCredentials => Unauthorized("Invalid token.") + case UserNotFound => NotFound("User not found.") + case UserNotActive => Forbidden("User not active.") + } +} + +object V3BaseEndpoint { + val layer = ZLayer.derive[V3BaseEndpoint] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorCode.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorCode.scala new file mode 100644 index 0000000000..5aaa4b3603 --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorCode.scala @@ -0,0 +1,17 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 +import zio.json.JsonCodec + +enum V3ErrorCode: + case resource_not_found + +object V3ErrorCode: + given JsonCodec[V3ErrorCode] = JsonCodec.string + .transformOrFail( + str => V3ErrorCode.values.find(_.toString == str).toRight(s"Unknown V3ErrorCode: $str"), + _.toString.toLowerCase, + ) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorInfo.scala b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorInfo.scala new file mode 100644 index 0000000000..4c9f9c526a --- /dev/null +++ b/webapi/src/main/scala/org/knora/webapi/slice/api/v3/V3ErrorInfo.scala @@ -0,0 +1,52 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 + +import zio.Chunk +import zio.json.* + +import org.knora.webapi.slice.common.KnoraIris.ResourceIri + +sealed trait V3ErrorInfo { + def message: String + def errors: Chunk[ErrorDetail] +} + +case class NotFound(message: String = "Not Found", errors: Chunk[ErrorDetail] = Chunk.empty) extends V3ErrorInfo +object NotFound { + + private def singleError(code: V3ErrorCode, message: String, details: Map[String, String] = Map.empty): NotFound = + NotFound(message, Chunk(ErrorDetail(code, message, details))) + + def from(resourceIri: ResourceIri): NotFound = + singleError( + V3ErrorCode.resource_not_found, + s"The resource with IRI '$resourceIri' was not found.", + Map("resourceIri" -> resourceIri.toString), + ) +} + +case class BadRequest(message: String = "Bad Request", errors: Chunk[ErrorDetail] = Chunk.empty) extends V3ErrorInfo + +final case class Unauthorized(message: String = "Unauthorized", errors: Chunk[ErrorDetail] = Chunk.empty) + extends V3ErrorInfo +final case class Forbidden(message: String = "Forbidden", errors: Chunk[ErrorDetail] = Chunk.empty) extends V3ErrorInfo + +final case class ErrorDetail( + code: V3ErrorCode, + message: String, + details: Map[String, String] = Map.empty, +) +object ErrorDetail { + given errorDetailEncoder: JsonCodec[ErrorDetail] = DeriveJsonCodec.gen[ErrorDetail] +} + +object V3ErrorInfo { + given notFoundEncoder: JsonCodec[NotFound] = DeriveJsonCodec.gen[NotFound] + given badRequestEncoder: JsonCodec[BadRequest] = DeriveJsonCodec.gen[BadRequest] + given unauthorizedEncoder: JsonCodec[Unauthorized] = DeriveJsonCodec.gen[Unauthorized] + given forbiddenEncoder: JsonCodec[Forbidden] = DeriveJsonCodec.gen[Forbidden] +} diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala index 48a749388c..9f1d212056 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala @@ -20,15 +20,12 @@ import zio.metrics.jvm.DefaultJvmMetrics import org.knora.webapi.config.InstrumentationServerConfig import org.knora.webapi.config.KnoraApi import org.knora.webapi.core.State -import org.knora.webapi.slice.admin.api.AdminApiEndpoints -import org.knora.webapi.slice.common.api.ApiV2Endpoints +import org.knora.webapi.routing.Endpoints import org.knora.webapi.slice.infrastructure.api.PrometheusRoutes -import org.knora.webapi.slice.shacl.api.ShaclEndpoints object MetricsServer { - private val metricsServer - : ZIO[AdminApiEndpoints & ApiV2Endpoints & KnoraApi & ShaclEndpoints & PrometheusRoutes & Server, Nothing, Unit] = + private val metricsServer: ZIO[Endpoints & KnoraApi & PrometheusRoutes & Server, Nothing, Unit] = for { docs <- DocsServer.docsEndpoints.map(endpoints => ZioHttpInterpreter().toHttp(endpoints)) prometheus <- ZIO.service[PrometheusRoutes] @@ -36,20 +33,17 @@ object MetricsServer { _ <- ZIO.never.unit } yield () - type MetricsServerEnv = KnoraApi & State & InstrumentationServerConfig & ApiV2Endpoints & ShaclEndpoints & - AdminApiEndpoints + type MetricsServerEnv = KnoraApi & State & InstrumentationServerConfig & Endpoints val make: ZIO[MetricsServerEnv, Throwable, Unit] = for { - _ <- ZIO.logInfo("Starting metrics and docs server...") - knoraApiConfig <- ZIO.service[KnoraApi] - apiV2Endpoints <- ZIO.service[ApiV2Endpoints] - adminApiEndpoints <- ZIO.service[AdminApiEndpoints] - shaclApiEndpoints <- ZIO.service[ShaclEndpoints] - config <- ZIO.service[InstrumentationServerConfig] - port = config.port - interval = config.interval - metricsConfig = MetricsConfig(interval) + _ <- ZIO.logInfo("Starting metrics and docs server...") + knoraApiConfig <- ZIO.service[KnoraApi] + endpoints <- ZIO.service[Endpoints] + config <- ZIO.service[InstrumentationServerConfig] + port = config.port + interval = config.interval + metricsConfig = MetricsConfig(interval) _ <- ZIO.logInfo( s"Docs and metrics available at " + @@ -58,9 +52,7 @@ object MetricsServer { ) _ <- metricsServer.provide( ZLayer.succeed(knoraApiConfig), - ZLayer.succeed(adminApiEndpoints), - ZLayer.succeed(apiV2Endpoints), - ZLayer.succeed(shaclApiEndpoints), + ZLayer.succeed(endpoints), Server.defaultWithPort(port), prometheus.publisherLayer, ZLayer.succeed(metricsConfig) >>> prometheus.prometheusLayer, @@ -76,11 +68,8 @@ object DocsServer { val docsEndpoints = for { - config <- ZIO.service[KnoraApi] - apiV2 <- ZIO.serviceWith[ApiV2Endpoints](_.endpoints) - admin <- ZIO.serviceWith[AdminApiEndpoints](_.endpoints) - shacl <- ZIO.serviceWith[ShaclEndpoints](_.endpoints) - allEndpoints = List(apiV2, admin, shacl).flatten + config <- ZIO.service[KnoraApi] + serverEndpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) info = Info( title = "DSP-API", version = BuildInfo.version, @@ -90,7 +79,7 @@ object DocsServer { contact = Some(Contact(name = Some("DaSCH"), url = Some("https://www.dasch.swiss/"))), ) } yield SwaggerInterpreter(customiseDocsModel = addServer(config)) - .fromEndpoints[Task](allEndpoints, info) + .fromServerEndpoints[Task](serverEndpoints, info) private def addServer(config: KnoraApi) = (openApi: OpenAPI) => { openApi.copy(servers = diff --git a/webapi/src/test/scala/org/knora/webapi/slice/api/v3/NotFoundSpec.scala b/webapi/src/test/scala/org/knora/webapi/slice/api/v3/NotFoundSpec.scala new file mode 100644 index 0000000000..5389889db5 --- /dev/null +++ b/webapi/src/test/scala/org/knora/webapi/slice/api/v3/NotFoundSpec.scala @@ -0,0 +1,36 @@ +/* + * Copyright © 2021 - 2025 Swiss National Data and Service Center for the Humanities and/or DaSCH Service Platform contributors. + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.knora.webapi.slice.api.v3 +import zio.* +import zio.json.* +import zio.test.* + +import org.knora.webapi.messages.StringFormatter +import org.knora.webapi.slice.common.service.IriConverter + +object NotFoundSpec extends ZIOSpecDefault { + override val spec = suite("NotFoundSpec")( + test("NotFound.from(ResourceIri) should create a NotFound instance with the correct message and error details") { + for { + resourceIri <- ZIO.serviceWithZIO[IriConverter](_.asResourceIri("http://rdfh.ch/0001/abcd1234")) + actual = NotFound.from(resourceIri) + } yield assertTrue( + actual.toJsonPretty == """{ + | "message" : "The resource with IRI 'http://rdfh.ch/0001/abcd1234' was not found.", + | "errors" : [ + | { + | "code" : "resource_not_found", + | "message" : "The resource with IRI 'http://rdfh.ch/0001/abcd1234' was not found.", + | "details" : { + | "resourceIri" : "http://rdfh.ch/0001/abcd1234" + | } + | } + | ] + |}""".stripMargin, + ) + }, + ).provide(IriConverter.layer, StringFormatter.test) +} From c946df6d58d48c6cefea2ba0281196d268b5d46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Kleinb=C3=B6lting?= Date: Mon, 13 Oct 2025 17:09:18 +0200 Subject: [PATCH 2/2] Revert building openapi yaml from server endpoints --- .../slice/infrastructure/MetricsServer.scala | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala index 9f1d212056..48a749388c 100644 --- a/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala +++ b/webapi/src/main/scala/org/knora/webapi/slice/infrastructure/MetricsServer.scala @@ -20,12 +20,15 @@ import zio.metrics.jvm.DefaultJvmMetrics import org.knora.webapi.config.InstrumentationServerConfig import org.knora.webapi.config.KnoraApi import org.knora.webapi.core.State -import org.knora.webapi.routing.Endpoints +import org.knora.webapi.slice.admin.api.AdminApiEndpoints +import org.knora.webapi.slice.common.api.ApiV2Endpoints import org.knora.webapi.slice.infrastructure.api.PrometheusRoutes +import org.knora.webapi.slice.shacl.api.ShaclEndpoints object MetricsServer { - private val metricsServer: ZIO[Endpoints & KnoraApi & PrometheusRoutes & Server, Nothing, Unit] = + private val metricsServer + : ZIO[AdminApiEndpoints & ApiV2Endpoints & KnoraApi & ShaclEndpoints & PrometheusRoutes & Server, Nothing, Unit] = for { docs <- DocsServer.docsEndpoints.map(endpoints => ZioHttpInterpreter().toHttp(endpoints)) prometheus <- ZIO.service[PrometheusRoutes] @@ -33,17 +36,20 @@ object MetricsServer { _ <- ZIO.never.unit } yield () - type MetricsServerEnv = KnoraApi & State & InstrumentationServerConfig & Endpoints + type MetricsServerEnv = KnoraApi & State & InstrumentationServerConfig & ApiV2Endpoints & ShaclEndpoints & + AdminApiEndpoints val make: ZIO[MetricsServerEnv, Throwable, Unit] = for { - _ <- ZIO.logInfo("Starting metrics and docs server...") - knoraApiConfig <- ZIO.service[KnoraApi] - endpoints <- ZIO.service[Endpoints] - config <- ZIO.service[InstrumentationServerConfig] - port = config.port - interval = config.interval - metricsConfig = MetricsConfig(interval) + _ <- ZIO.logInfo("Starting metrics and docs server...") + knoraApiConfig <- ZIO.service[KnoraApi] + apiV2Endpoints <- ZIO.service[ApiV2Endpoints] + adminApiEndpoints <- ZIO.service[AdminApiEndpoints] + shaclApiEndpoints <- ZIO.service[ShaclEndpoints] + config <- ZIO.service[InstrumentationServerConfig] + port = config.port + interval = config.interval + metricsConfig = MetricsConfig(interval) _ <- ZIO.logInfo( s"Docs and metrics available at " + @@ -52,7 +58,9 @@ object MetricsServer { ) _ <- metricsServer.provide( ZLayer.succeed(knoraApiConfig), - ZLayer.succeed(endpoints), + ZLayer.succeed(adminApiEndpoints), + ZLayer.succeed(apiV2Endpoints), + ZLayer.succeed(shaclApiEndpoints), Server.defaultWithPort(port), prometheus.publisherLayer, ZLayer.succeed(metricsConfig) >>> prometheus.prometheusLayer, @@ -68,8 +76,11 @@ object DocsServer { val docsEndpoints = for { - config <- ZIO.service[KnoraApi] - serverEndpoints <- ZIO.serviceWith[Endpoints](_.serverEndpoints) + config <- ZIO.service[KnoraApi] + apiV2 <- ZIO.serviceWith[ApiV2Endpoints](_.endpoints) + admin <- ZIO.serviceWith[AdminApiEndpoints](_.endpoints) + shacl <- ZIO.serviceWith[ShaclEndpoints](_.endpoints) + allEndpoints = List(apiV2, admin, shacl).flatten info = Info( title = "DSP-API", version = BuildInfo.version, @@ -79,7 +90,7 @@ object DocsServer { contact = Some(Contact(name = Some("DaSCH"), url = Some("https://www.dasch.swiss/"))), ) } yield SwaggerInterpreter(customiseDocsModel = addServer(config)) - .fromServerEndpoints[Task](serverEndpoints, info) + .fromEndpoints[Task](allEndpoints, info) private def addServer(config: KnoraApi) = (openApi: OpenAPI) => { openApi.copy(servers =