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-dsl" % http4sVersion,
|
||||||
"org.http4s" %% "http4s-circe" % http4sVersion,
|
"org.http4s" %% "http4s-circe" % http4sVersion,
|
||||||
"io.circe" %% "circe-generic" % circeVersion,
|
"io.circe" %% "circe-generic" % circeVersion,
|
||||||
"co.fs2" %% "fs2-io" % "3.10.1",
|
"co.fs2" %% "fs2-io" % "3.12.0",
|
||||||
|
|
||||||
// Tapir + Swagger
|
// Tapir + Swagger
|
||||||
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion,
|
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % tapirVersion,
|
||||||
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
|
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
|
||||||
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
|
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
|
||||||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % 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")
|
.withHost(host"0.0.0.0")
|
||||||
.withPort(Port.fromInt(AppConfig.PORT).get)
|
.withPort(Port.fromInt(AppConfig.PORT).get)
|
||||||
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(routes.all.orNotFound))
|
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(routes.all.orNotFound))
|
||||||
|
// .withTLS()
|
||||||
|
.withHttp2
|
||||||
.build
|
.build
|
||||||
}.use(_ => // ✅ внутри use
|
}.use(_ => // ✅ внутри use
|
||||||
IO.println(s"Server running at http://${AppConfig.HOST}:${AppConfig.PORT}") *>
|
IO.println(s"Server running at http://0.0.0.0:${AppConfig.PORT}") *>
|
||||||
IO.println(s"Swagger UI available at http://${AppConfig.HOST}:${AppConfig.PORT}/docs") *>
|
IO.println(s"Swagger UI available at http://0.0.0.0:${AppConfig.PORT}/docs") *>
|
||||||
IO.never
|
IO.never
|
||||||
).as(ExitCode.Success)
|
).as(ExitCode.Success)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,27 +11,93 @@ import org.yobble.module.util.ErrorExamples
|
|||||||
|
|
||||||
object TestErrorEndpoint {
|
object TestErrorEndpoint {
|
||||||
|
|
||||||
val testErrorEndpoint: Endpoint[String, Option[Int], ErrorResponse, BaseResponse, Any] =
|
val testErrorEndpoint: Endpoint[String, Option[Int], (StatusCode, ErrorResponse), BaseResponse, Any] =
|
||||||
endpoint.get
|
endpoint.get
|
||||||
.in("test-error")
|
.in("test-error")
|
||||||
.name("Test Error")
|
.name("Test Error")
|
||||||
.securityIn(auth.bearer[String](WWWAuthenticateChallenge.bearer))
|
.securityIn(auth.bearer[String](WWWAuthenticateChallenge.bearer))
|
||||||
.in(
|
.in(query[Option[Int]]("code").description("Optional HTTP status code").example(Some(404)))
|
||||||
query[Option[Int]]("code")
|
|
||||||
.description("Optional HTTP status code to simulate. Defaults to 500.")
|
|
||||||
.example(Some(404))
|
|
||||||
)
|
|
||||||
.errorOut(
|
.errorOut(
|
||||||
oneOf[ErrorResponse](
|
oneOf[(StatusCode, ErrorResponse)](
|
||||||
oneOfVariant(StatusCode.BadRequest, jsonBody[ErrorResponse].description("Bad Request").example(ErrorExamples.badRequest)),
|
oneOfVariantValueMatcher(
|
||||||
oneOfVariant(StatusCode.Unauthorized, jsonBody[ErrorResponse].description("Unauthorized").example(ErrorExamples.unauthorized)),
|
statusCode.and(
|
||||||
oneOfVariant(StatusCode.Forbidden, jsonBody[ErrorResponse].description("Forbidden").example(ErrorExamples.forbidden)),
|
jsonBody[ErrorResponse]
|
||||||
oneOfVariant(StatusCode.NotFound, jsonBody[ErrorResponse].description("Not Found").example(ErrorExamples.notFound)),
|
.description("Bad Request")
|
||||||
oneOfVariant(StatusCode.MethodNotAllowed, jsonBody[ErrorResponse].description("Method Not Allowed").example(ErrorExamples.notAllowed)),
|
.example(ErrorExamples.badRequest)
|
||||||
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)),
|
) { case (code, _) => code == StatusCode.BadRequest },
|
||||||
oneOfVariant(StatusCode.UnprocessableEntity, jsonBody[ErrorResponse].description("Validation Error").example(ErrorExamples.validation)),
|
|
||||||
oneOfVariant(StatusCode.InternalServerError, jsonBody[ErrorResponse].description("Internal Server Error").example(ErrorExamples.internal))
|
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])
|
.out(jsonBody[BaseResponse])
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import org.yobble.module.service.profile.ProfileClient
|
|||||||
import org.yobble.module.util.errorByStatus
|
import org.yobble.module.util.errorByStatus
|
||||||
import sttp.model.StatusCode
|
import sttp.model.StatusCode
|
||||||
import sttp.tapir.server.ServerEndpoint
|
import sttp.tapir.server.ServerEndpoint
|
||||||
|
import sttp.tapir.server.ServerEndpoint.Full
|
||||||
|
|
||||||
object AllServerEndpoints {
|
object AllServerEndpoints {
|
||||||
def all(authClient: AuthClient, profileClient: ProfileClient): List[ServerEndpoint[Any, IO]] = List(
|
def all(authClient: AuthClient, profileClient: ProfileClient): List[ServerEndpoint[Any, IO]] = List(
|
||||||
@ -23,27 +24,34 @@ object AllServerEndpoints {
|
|||||||
|
|
||||||
authClient.getCurrentUser(cleanToken, ip, ua).attempt.map {
|
authClient.getCurrentUser(cleanToken, ip, ua).attempt.map {
|
||||||
case Left(TokenServiceException(code, msg)) =>
|
case Left(TokenServiceException(code, msg)) =>
|
||||||
Left(ErrorResponse(
|
Left((code, ErrorResponse(
|
||||||
status = code.code.toString,
|
status = code.code.toString,
|
||||||
errors = List(ErrorDetail(field = "Authorization", message = msg))
|
errors = List(ErrorDetail(field = "Authorization", message = msg))
|
||||||
))
|
)))
|
||||||
|
|
||||||
case Left(other) =>
|
// case Left(_) =>
|
||||||
Left(ErrorResponse(
|
// val code = StatusCode.ServiceUnavailable
|
||||||
status = StatusCode.InternalServerError.code.toString,
|
// Left((code, errorByStatus(code))) // <-- Исправлено!
|
||||||
errors = List(ErrorDetail(field = "Authorization", message = "token_service: service unavailable"))
|
|
||||||
))
|
case Left(_) =>
|
||||||
|
val code = StatusCode.ServiceUnavailable
|
||||||
|
Left((code, ErrorResponse(
|
||||||
|
status = "error",
|
||||||
|
errors = List(ErrorDetail(field = "request", message = "Token service unavailable"))
|
||||||
|
)))
|
||||||
|
|
||||||
case Right(_) =>
|
case Right(_) =>
|
||||||
Right(token)
|
Right(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.serverLogic { _ => maybeCode =>
|
.serverLogic { _ =>
|
||||||
|
maybeCode =>
|
||||||
val status = maybeCode
|
val status = maybeCode
|
||||||
.flatMap(code => StatusCode.safeApply(code).toOption)
|
.flatMap(code => StatusCode.safeApply(code).toOption)
|
||||||
.getOrElse(StatusCode.InternalServerError)
|
.getOrElse(StatusCode.InternalServerError)
|
||||||
|
|
||||||
IO.pure(Left(errorByStatus(status)))
|
IO.pure(Left((status, errorByStatus(status)))) // ❗ важно: кортеж!
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,52 @@
|
|||||||
package org.yobble.module.service
|
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.client.Client
|
||||||
import org.http4s.ember.client.EmberClientBuilder
|
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 {
|
object HttpClient {
|
||||||
def create: Resource[IO, Client[IO]] =
|
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]
|
.default[IO]
|
||||||
|
.withTLSContext(tlsContext)
|
||||||
|
.withHttp2
|
||||||
.build
|
.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 =
|
def internalServerError(message: String = "Internal Server Error"): ErrorResponse =
|
||||||
ErrorResponse(errors = List(ErrorDetail("server", message)))
|
ErrorResponse(errors = List(ErrorDetail("server", message)))
|
||||||
|
|
||||||
|
def serviceUnavailableError(message: String = "Service unavailable"): ErrorResponse =
|
||||||
|
ErrorResponse(errors = List(ErrorDetail("request", message)))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object ErrorExamples {
|
object ErrorExamples {
|
||||||
@ -79,6 +83,11 @@ object ErrorExamples {
|
|||||||
status = "error",
|
status = "error",
|
||||||
errors = List(ErrorDetail("server", "An unexpected error occurred. Please try again later."))
|
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
|
// 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.code == 418 => ErrorExamples.teapot
|
||||||
case c if c == StatusCode.UnprocessableEntity => ErrorExamples.validation
|
case c if c == StatusCode.UnprocessableEntity => ErrorExamples.validation
|
||||||
case c if c == StatusCode.InternalServerError => ErrorExamples.internal
|
case c if c == StatusCode.InternalServerError => ErrorExamples.internal
|
||||||
|
case c if c == StatusCode.ServiceUnavailable => ErrorExamples.unavailable
|
||||||
case _ => ErrorExamples.internal
|
case _ => ErrorExamples.internal
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user