update
This commit is contained in:
parent
dd388ca2c1
commit
00da0e3303
@ -17,13 +17,17 @@ lazy val root = (project in file("."))
|
||||
"org.http4s" %% "http4s-dsl" % http4sVersion,
|
||||
"org.http4s" %% "http4s-circe" % http4sVersion,
|
||||
"io.circe" %% "circe-generic" % circeVersion,
|
||||
"co.fs2" %% "fs2-io" % "3.10.1",
|
||||
"co.fs2" %% "fs2-io" % "3.12.0",
|
||||
|
||||
// Tapir + Swagger
|
||||
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion,
|
||||
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
|
||||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
|
||||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % tapirVersion,
|
||||
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.11.10"
|
||||
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.11.10",
|
||||
|
||||
// logs
|
||||
"org.typelevel" %% "log4cats-slf4j" % "2.6.0",
|
||||
"ch.qos.logback" % "logback-classic" % "1.4.11"
|
||||
)
|
||||
)
|
||||
|
||||
@ -23,10 +23,12 @@ object Main extends IOApp {
|
||||
.withHost(host"0.0.0.0")
|
||||
.withPort(Port.fromInt(AppConfig.PORT).get)
|
||||
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(routes.all.orNotFound))
|
||||
// .withTLS()
|
||||
.withHttp2
|
||||
.build
|
||||
}.use(_ => // ✅ внутри use
|
||||
IO.println(s"Server running at http://${AppConfig.HOST}:${AppConfig.PORT}") *>
|
||||
IO.println(s"Swagger UI available at http://${AppConfig.HOST}:${AppConfig.PORT}/docs") *>
|
||||
IO.println(s"Server running at http://0.0.0.0:${AppConfig.PORT}") *>
|
||||
IO.println(s"Swagger UI available at http://0.0.0.0:${AppConfig.PORT}/docs") *>
|
||||
IO.never
|
||||
).as(ExitCode.Success)
|
||||
}
|
||||
|
||||
@ -11,27 +11,93 @@ import org.yobble.module.util.ErrorExamples
|
||||
|
||||
object TestErrorEndpoint {
|
||||
|
||||
val testErrorEndpoint: Endpoint[String, Option[Int], ErrorResponse, BaseResponse, Any] =
|
||||
val testErrorEndpoint: Endpoint[String, Option[Int], (StatusCode, ErrorResponse), BaseResponse, Any] =
|
||||
endpoint.get
|
||||
.in("test-error")
|
||||
.name("Test Error")
|
||||
.securityIn(auth.bearer[String](WWWAuthenticateChallenge.bearer))
|
||||
.in(
|
||||
query[Option[Int]]("code")
|
||||
.description("Optional HTTP status code to simulate. Defaults to 500.")
|
||||
.example(Some(404))
|
||||
)
|
||||
.in(query[Option[Int]]("code").description("Optional HTTP status code").example(Some(404)))
|
||||
.errorOut(
|
||||
oneOf[ErrorResponse](
|
||||
oneOfVariant(StatusCode.BadRequest, jsonBody[ErrorResponse].description("Bad Request").example(ErrorExamples.badRequest)),
|
||||
oneOfVariant(StatusCode.Unauthorized, jsonBody[ErrorResponse].description("Unauthorized").example(ErrorExamples.unauthorized)),
|
||||
oneOfVariant(StatusCode.Forbidden, jsonBody[ErrorResponse].description("Forbidden").example(ErrorExamples.forbidden)),
|
||||
oneOfVariant(StatusCode.NotFound, jsonBody[ErrorResponse].description("Not Found").example(ErrorExamples.notFound)),
|
||||
oneOfVariant(StatusCode.MethodNotAllowed, jsonBody[ErrorResponse].description("Method Not Allowed").example(ErrorExamples.notAllowed)),
|
||||
oneOfVariant(StatusCode.Conflict, jsonBody[ErrorResponse].description("Conflict").example(ErrorExamples.conflict)),
|
||||
oneOfVariant(StatusCode(418), jsonBody[ErrorResponse].description("I'm a teapot (In Development)").example(ErrorExamples.teapot)),
|
||||
oneOfVariant(StatusCode.UnprocessableEntity, jsonBody[ErrorResponse].description("Validation Error").example(ErrorExamples.validation)),
|
||||
oneOfVariant(StatusCode.InternalServerError, jsonBody[ErrorResponse].description("Internal Server Error").example(ErrorExamples.internal))
|
||||
oneOf[(StatusCode, ErrorResponse)](
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Bad Request")
|
||||
.example(ErrorExamples.badRequest)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.BadRequest },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Unauthorized")
|
||||
.example(ErrorExamples.unauthorized)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.Unauthorized },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Forbidden")
|
||||
.example(ErrorExamples.forbidden)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.Forbidden },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Not Found")
|
||||
.example(ErrorExamples.notFound)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.NotFound },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Method Not Allowed")
|
||||
.example(ErrorExamples.notAllowed)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.MethodNotAllowed },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Conflict")
|
||||
.example(ErrorExamples.conflict)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.Conflict },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("I'm a teapot (In Development)")
|
||||
.example(ErrorExamples.teapot)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode(418) },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Validation Error")
|
||||
.example(ErrorExamples.validation)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.UnprocessableEntity },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Internal Server Error")
|
||||
.example(ErrorExamples.internal)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.InternalServerError },
|
||||
|
||||
oneOfVariantValueMatcher(
|
||||
statusCode.and(
|
||||
jsonBody[ErrorResponse]
|
||||
.description("Service Unavailable")
|
||||
.example(ErrorExamples.unavailable)
|
||||
)
|
||||
) { case (code, _) => code == StatusCode.ServiceUnavailable }
|
||||
)
|
||||
)
|
||||
.out(jsonBody[BaseResponse])
|
||||
|
||||
@ -8,6 +8,7 @@ import org.yobble.module.service.profile.ProfileClient
|
||||
import org.yobble.module.util.errorByStatus
|
||||
import sttp.model.StatusCode
|
||||
import sttp.tapir.server.ServerEndpoint
|
||||
import sttp.tapir.server.ServerEndpoint.Full
|
||||
|
||||
object AllServerEndpoints {
|
||||
def all(authClient: AuthClient, profileClient: ProfileClient): List[ServerEndpoint[Any, IO]] = List(
|
||||
@ -23,27 +24,34 @@ object AllServerEndpoints {
|
||||
|
||||
authClient.getCurrentUser(cleanToken, ip, ua).attempt.map {
|
||||
case Left(TokenServiceException(code, msg)) =>
|
||||
Left(ErrorResponse(
|
||||
Left((code, ErrorResponse(
|
||||
status = code.code.toString,
|
||||
errors = List(ErrorDetail(field = "Authorization", message = msg))
|
||||
))
|
||||
)))
|
||||
|
||||
case Left(other) =>
|
||||
Left(ErrorResponse(
|
||||
status = StatusCode.InternalServerError.code.toString,
|
||||
errors = List(ErrorDetail(field = "Authorization", message = "token_service: service unavailable"))
|
||||
))
|
||||
// case Left(_) =>
|
||||
// val code = StatusCode.ServiceUnavailable
|
||||
// Left((code, errorByStatus(code))) // <-- Исправлено!
|
||||
|
||||
case Left(_) =>
|
||||
val code = StatusCode.ServiceUnavailable
|
||||
Left((code, ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail(field = "request", message = "Token service unavailable"))
|
||||
)))
|
||||
|
||||
case Right(_) =>
|
||||
Right(token)
|
||||
}
|
||||
}
|
||||
.serverLogic { _ => maybeCode =>
|
||||
.serverLogic { _ =>
|
||||
maybeCode =>
|
||||
val status = maybeCode
|
||||
.flatMap(code => StatusCode.safeApply(code).toOption)
|
||||
.getOrElse(StatusCode.InternalServerError)
|
||||
|
||||
IO.pure(Left(errorByStatus(status)))
|
||||
IO.pure(Left((status, errorByStatus(status)))) // ❗ важно: кортеж!
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,52 @@
|
||||
package org.yobble.module.service
|
||||
|
||||
import cats.effect.*
|
||||
import cats.effect.{IO, Resource}
|
||||
import fs2.io.net.tls.TLSContext
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.ember.client.EmberClientBuilder
|
||||
import org.http4s.headers.`User-Agent`
|
||||
|
||||
import java.io.FileInputStream
|
||||
import scala.util.chaining.*
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.{Certificate, CertificateFactory}
|
||||
import javax.net.ssl.{SSLContext, TrustManagerFactory}
|
||||
|
||||
|
||||
object HttpClient {
|
||||
def create: Resource[IO, Client[IO]] =
|
||||
EmberClientBuilder
|
||||
for {
|
||||
// tlsContext <- customTLSContext("/home/cardinalnsk/IdeaProjects/chat_private_service/src/main/resources/fullchain.pem")
|
||||
tlsContext <- Resource.eval(TLSContext.Builder.forAsync[IO].insecure)
|
||||
client <- EmberClientBuilder
|
||||
.default[IO]
|
||||
.withTLSContext(tlsContext)
|
||||
.withHttp2
|
||||
.build
|
||||
} yield client
|
||||
|
||||
private def customTLSContext(certPath: String): Resource[IO, TLSContext[IO]] =
|
||||
Resource.eval {
|
||||
IO.blocking {
|
||||
// 1. Загружаем X.509 сертификат из файла
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val cert: Certificate = certFactory.generateCertificate(new FileInputStream(certPath))
|
||||
|
||||
// 2. Создаём пустой KeyStore и добавляем туда сертификат
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
|
||||
keyStore.load(null, null)
|
||||
keyStore.setCertificateEntry("custom-ca", cert)
|
||||
|
||||
// 3. Инициализируем TrustManagerFactory этим keystore
|
||||
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
|
||||
tmf.init(keyStore)
|
||||
|
||||
// 4. Создаём SSLContext, используя TrustManager
|
||||
val sslContext = SSLContext.getInstance("TLS")
|
||||
sslContext.init(null, tmf.getTrustManagers, new java.security.SecureRandom())
|
||||
|
||||
// Новый способ: TLSContext.Builder.forAsyncWithSSLContext
|
||||
TLSContext.Builder.forAsync[IO].fromSSLContext(sslContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,10 @@ object ErrorUtils {
|
||||
|
||||
def internalServerError(message: String = "Internal Server Error"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("server", message)))
|
||||
|
||||
def serviceUnavailableError(message: String = "Service unavailable"): ErrorResponse =
|
||||
ErrorResponse(errors = List(ErrorDetail("request", message)))
|
||||
|
||||
}
|
||||
|
||||
object ErrorExamples {
|
||||
@ -79,6 +83,11 @@ object ErrorExamples {
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("server", "An unexpected error occurred. Please try again later."))
|
||||
)
|
||||
|
||||
val unavailable: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("request", "Service unavailable."))
|
||||
)
|
||||
}
|
||||
|
||||
// example use
|
||||
@ -93,5 +102,6 @@ def errorByStatus(code: StatusCode): ErrorResponse = code match {
|
||||
case c if c.code == 418 => ErrorExamples.teapot
|
||||
case c if c == StatusCode.UnprocessableEntity => ErrorExamples.validation
|
||||
case c if c == StatusCode.InternalServerError => ErrorExamples.internal
|
||||
case c if c == StatusCode.ServiceUnavailable => ErrorExamples.unavailable
|
||||
case _ => ErrorExamples.internal
|
||||
}
|
||||
Reference in New Issue
Block a user