update
This commit is contained in:
parent
f3f458a1b0
commit
dd388ca2c1
@ -17,10 +17,13 @@ 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",
|
||||
|
||||
// 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-swagger-ui-bundle" % tapirVersion,
|
||||
"com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % tapirVersion,
|
||||
"com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.11.10"
|
||||
)
|
||||
)
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package org.yobble.chat_private_service
|
||||
|
||||
import cats.effect.*
|
||||
import org.http4s.client.Client
|
||||
import org.yobble.chat_private_service.api.route.Routes
|
||||
import org.yobble.module.service.auth.AuthClient
|
||||
import org.yobble.module.service.profile.ProfileClient
|
||||
import org.yobble.module.service.HttpClient
|
||||
|
||||
object AppModule {
|
||||
|
||||
def create: Resource[IO, Routes] =
|
||||
for {
|
||||
httpClient <- HttpClient.create
|
||||
authClient = new AuthClient(httpClient)
|
||||
profileClient = new ProfileClient(httpClient)
|
||||
routes = new Routes(authClient, profileClient)
|
||||
} yield routes
|
||||
}
|
||||
@ -12,22 +12,21 @@ import org.http4s.server.Router
|
||||
import cats.implicits.toSemigroupKOps
|
||||
import com.comcast.ip4s.{Port, host}
|
||||
import org.http4s.circe.CirceEntityCodec.*
|
||||
import org.yobble.chat_private_service.api.Routes
|
||||
import org.yobble.chat_private_service.api.route.Routes
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import org.yobble.chat_private_service.middleware.GlobalErrorHandler
|
||||
import org.yobble.module.middleware.GlobalErrorHandler
|
||||
|
||||
object Main extends IOApp {
|
||||
override def run(args: List[String]): IO[ExitCode] =
|
||||
EmberServerBuilder
|
||||
.default[IO]
|
||||
.withHost(host"0.0.0.0")
|
||||
.withPort(port = Port.fromInt(AppConfig.PORT).get)
|
||||
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(Routes.all.orNotFound))
|
||||
.build
|
||||
.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.never
|
||||
)
|
||||
.as(ExitCode.Success)
|
||||
AppModule.create.flatMap { routes => // ✅ создаём routes тут
|
||||
EmberServerBuilder.default[IO]
|
||||
.withHost(host"0.0.0.0")
|
||||
.withPort(Port.fromInt(AppConfig.PORT).get)
|
||||
.withHttpApp(GlobalErrorHandler.withGlobalErrorHandler(routes.all.orNotFound))
|
||||
.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.never
|
||||
).as(ExitCode.Success)
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
package org.yobble.chat_private_service.api
|
||||
|
||||
import cats.effect.IO
|
||||
import org.http4s.HttpRoutes
|
||||
import sttp.tapir.server.http4s.*
|
||||
import sttp.tapir.swagger.bundle.SwaggerInterpreter
|
||||
import org.yobble.chat_private_service.api.endpoint.PingEndpoint.*
|
||||
import org.yobble.chat_private_service.api.endpoint.ErrorsEndpoint.*
|
||||
import org.yobble.chat_private_service.api.endpoint.TestErrorEndpoint.*
|
||||
import cats.syntax.semigroupk.*
|
||||
import org.yobble.chat_private_service.api.response.BaseResponse
|
||||
import sttp.model.StatusCode
|
||||
import org.yobble.chat_private_service.api.util.errorByStatus
|
||||
|
||||
object Routes {
|
||||
private val pingRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "pong")))
|
||||
)
|
||||
|
||||
private val errorsRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
errorsEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "errors")))
|
||||
)
|
||||
|
||||
private val testErrorRoute = Http4sServerInterpreter[IO]().toRoutes(
|
||||
testErrorEndpoint.serverLogic((code: Int) =>
|
||||
IO.pure(Left(errorByStatus(StatusCode.safeApply(code).getOrElse(StatusCode.InternalServerError))))
|
||||
)
|
||||
)
|
||||
|
||||
private val docsRoutes = Http4sServerInterpreter[IO]().toRoutes(
|
||||
SwaggerInterpreter().fromEndpoints[IO](
|
||||
List(pingEndpoint, errorsEndpoint, testErrorEndpoint),
|
||||
"chat_private_service API",
|
||||
"1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val all: HttpRoutes[IO] = pingRoute <+> errorsRoute <+> testErrorRoute <+> docsRoutes
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
package org.yobble.chat_private_service.api.endpoint
|
||||
|
||||
import io.circe.generic.auto.*
|
||||
import org.yobble.chat_private_service.api.response.BaseResponse
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import org.yobble.module.response.{BaseResponse, ErrorResponse}
|
||||
import sttp.tapir.*
|
||||
import sttp.tapir.generic.auto.*
|
||||
import sttp.tapir.json.circe.*
|
||||
import org.yobble.chat_private_service.api.response.ErrorResponse
|
||||
import org.yobble.chat_private_service.api.util.ErrorExamples
|
||||
import org.yobble.module.util.ErrorExamples
|
||||
import sttp.model.StatusCode
|
||||
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ import sttp.tapir._
|
||||
import sttp.tapir.generic.auto._
|
||||
import sttp.tapir.json.circe._
|
||||
import io.circe.generic.auto._
|
||||
import org.yobble.chat_private_service.api.response.BaseResponse
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import org.yobble.module.response.BaseResponse
|
||||
|
||||
object PingEndpoint {
|
||||
|
||||
|
||||
@ -1,21 +1,26 @@
|
||||
package org.yobble.chat_private_service.api.endpoint
|
||||
|
||||
import io.circe.generic.auto.*
|
||||
import org.yobble.chat_private_service.api.response.{BaseResponse, ErrorResponse}
|
||||
import org.yobble.chat_private_service.api.util.ErrorExamples
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import sttp.model.StatusCode
|
||||
import sttp.tapir.*
|
||||
import sttp.tapir.generic.auto.*
|
||||
import sttp.tapir.json.circe.*
|
||||
|
||||
import sttp.model.headers.WWWAuthenticateChallenge
|
||||
import org.yobble.module.response.{BaseResponse, ErrorResponse}
|
||||
import org.yobble.module.util.ErrorExamples
|
||||
|
||||
object TestErrorEndpoint {
|
||||
|
||||
val testErrorEndpoint: Endpoint[Unit, Int, ErrorResponse, BaseResponse, Any] =
|
||||
val testErrorEndpoint: Endpoint[String, Option[Int], ErrorResponse, BaseResponse, Any] =
|
||||
endpoint.get
|
||||
.in("test-error")
|
||||
.in(query[Int]("code").description("HTTP status code to simulate (e.g., 401, 404)"))
|
||||
.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))
|
||||
)
|
||||
.errorOut(
|
||||
oneOf[ErrorResponse](
|
||||
oneOfVariant(StatusCode.BadRequest, jsonBody[ErrorResponse].description("Bad Request").example(ErrorExamples.badRequest)),
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
package org.yobble.chat_private_service.api.route
|
||||
|
||||
import cats.effect.IO
|
||||
import org.yobble.chat_private_service.api.endpoint.{ErrorsEndpoint, PingEndpoint, TestErrorEndpoint}
|
||||
import org.yobble.module.response.{BaseResponse, ErrorDetail, ErrorResponse}
|
||||
import org.yobble.module.service.auth.{AuthClient, TokenServiceException}
|
||||
import org.yobble.module.service.profile.ProfileClient
|
||||
import org.yobble.module.util.errorByStatus
|
||||
import sttp.model.StatusCode
|
||||
import sttp.tapir.server.ServerEndpoint
|
||||
|
||||
object AllServerEndpoints {
|
||||
def all(authClient: AuthClient, profileClient: ProfileClient): List[ServerEndpoint[Any, IO]] = List(
|
||||
PingEndpoint.pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "pong"))),
|
||||
|
||||
ErrorsEndpoint.errorsEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse("fine", "errors"))),
|
||||
|
||||
TestErrorEndpoint.testErrorEndpoint
|
||||
.serverSecurityLogic { token =>
|
||||
val cleanToken = token.stripPrefix("Bearer ").trim
|
||||
val ip = "127.0.0.1"
|
||||
val ua = "scala-client"
|
||||
|
||||
authClient.getCurrentUser(cleanToken, ip, ua).attempt.map {
|
||||
case Left(TokenServiceException(code, msg)) =>
|
||||
Left(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 Right(_) =>
|
||||
Right(token)
|
||||
}
|
||||
}
|
||||
.serverLogic { _ => maybeCode =>
|
||||
val status = maybeCode
|
||||
.flatMap(code => StatusCode.safeApply(code).toOption)
|
||||
.getOrElse(StatusCode.InternalServerError)
|
||||
|
||||
IO.pure(Left(errorByStatus(status)))
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package org.yobble.chat_private_service.api.route
|
||||
|
||||
import cats.effect.IO
|
||||
import cats.syntax.semigroupk.*
|
||||
import org.http4s.HttpRoutes
|
||||
import org.yobble.module.service.auth.AuthClient
|
||||
import org.yobble.module.service.profile.ProfileClient
|
||||
import sttp.tapir.server.http4s.Http4sServerInterpreter
|
||||
import sttp.tapir.swagger.bundle.SwaggerInterpreter
|
||||
|
||||
class Routes(authClient: AuthClient, profileClient: ProfileClient) {
|
||||
private val allServerEndpoints = AllServerEndpoints.all(authClient, profileClient)
|
||||
|
||||
private val httpRoutes = Http4sServerInterpreter[IO]().toRoutes(allServerEndpoints)
|
||||
|
||||
private val docsRoutes = Http4sServerInterpreter[IO]().toRoutes(
|
||||
SwaggerInterpreter().fromServerEndpoints[IO](
|
||||
allServerEndpoints,
|
||||
"chat_private_service API",
|
||||
"1.0"
|
||||
)
|
||||
)
|
||||
|
||||
val all: HttpRoutes[IO] = docsRoutes <+> httpRoutes
|
||||
}
|
||||
@ -5,4 +5,6 @@ object AppConfig {
|
||||
val HOST = "0.0.0.0"
|
||||
val PORT = 5205
|
||||
|
||||
val TOKEN_SERVICE = "https://localhost:5200"
|
||||
val PROFILE_SERVICE = ""
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
package org.yobble.chat_private_service.middleware
|
||||
|
||||
package org.yobble.module.middleware
|
||||
|
||||
import cats.effect.Sync
|
||||
import org.http4s.HttpApp
|
||||
import org.http4s.server.middleware.ErrorAction
|
||||
import org.yobble.chat_private_service.api.util.ErrorUtils
|
||||
import org.yobble.module.util.ErrorUtils
|
||||
|
||||
object GlobalErrorHandler {
|
||||
def withGlobalErrorHandler[F[_] : Sync](httpApp: HttpApp[F]): HttpApp[F] = {
|
||||
@ -1,3 +1,3 @@
|
||||
package org.yobble.chat_private_service.api.response
|
||||
package org.yobble.module.response
|
||||
|
||||
final case class BaseResponse(status: String, message: String)
|
||||
@ -1,4 +1,4 @@
|
||||
package org.yobble.chat_private_service.api.response
|
||||
package org.yobble.module.response
|
||||
|
||||
final case class ErrorDetail(field: String, message: String)
|
||||
final case class ErrorResponse(status: String = "error", errors: List[ErrorDetail])
|
||||
12
src/main/scala/org/yobble/module/service/HttpClient.scala
Normal file
12
src/main/scala/org/yobble/module/service/HttpClient.scala
Normal file
@ -0,0 +1,12 @@
|
||||
package org.yobble.module.service
|
||||
|
||||
import cats.effect.*
|
||||
import org.http4s.client.Client
|
||||
import org.http4s.ember.client.EmberClientBuilder
|
||||
|
||||
object HttpClient {
|
||||
def create: Resource[IO, Client[IO]] =
|
||||
EmberClientBuilder
|
||||
.default[IO]
|
||||
.build
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package org.yobble.module.service.auth
|
||||
|
||||
import cats.effect.IO
|
||||
import org.http4s.Method._
|
||||
import org.http4s.client.dsl.io._
|
||||
import org.http4s.client.Client
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._ // for .withEntity(json)
|
||||
import org.http4s.circe.CirceEntityDecoder._ // for .as[CurrentUser]
|
||||
import io.circe.syntax._ // for .asJson
|
||||
import io.circe.generic.auto._
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
import sttp.model.StatusCode
|
||||
|
||||
class AuthClient(client: Client[IO]) {
|
||||
|
||||
def getCurrentUser(token: String, ip: String, userAgent: String): IO[CurrentUser] = {
|
||||
val requestBody = DecodeTokenRequest(
|
||||
token = token,
|
||||
ip = ip,
|
||||
user_agent = userAgent,
|
||||
require_permissions = false
|
||||
).asJson
|
||||
|
||||
val req = Request[IO](
|
||||
method = POST,
|
||||
uri = Uri.unsafeFromString(AppConfig.TOKEN_SERVICE + "/decode")
|
||||
).withEntity(requestBody)
|
||||
|
||||
println(s"$req")
|
||||
|
||||
client.run(req).use { resp =>
|
||||
println(s"$resp")
|
||||
if (resp.status.isSuccess) {
|
||||
resp.as[CurrentUser]
|
||||
} else {
|
||||
resp.bodyText.compile.string.flatMap { body =>
|
||||
val status = StatusCode.safeApply(resp.status.code).getOrElse(StatusCode.InternalServerError)
|
||||
IO.raiseError(TokenServiceException(status, s"token_service: $body"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
package org.yobble.module.service.auth
|
||||
|
||||
case class CurrentUser(
|
||||
token: String,
|
||||
user_id: String,
|
||||
session_id: String,
|
||||
permissions: List[String]
|
||||
)
|
||||
@ -0,0 +1,8 @@
|
||||
package org.yobble.module.service.auth
|
||||
|
||||
case class DecodeTokenRequest(
|
||||
token: String,
|
||||
ip: String,
|
||||
user_agent: String,
|
||||
require_permissions: Boolean = false
|
||||
)
|
||||
@ -0,0 +1,12 @@
|
||||
package org.yobble.module.service.auth
|
||||
|
||||
import sttp.model.StatusCode
|
||||
|
||||
case class TokenServiceException(code: StatusCode, msg: String) extends Exception(msg)
|
||||
|
||||
object TokenServiceException {
|
||||
def unapply(e: Throwable): Option[(StatusCode, String)] = e match {
|
||||
case ex: TokenServiceException => Some((ex.code, ex.msg))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package org.yobble.module.service.profile
|
||||
|
||||
import cats.effect.IO
|
||||
import io.circe.syntax.*
|
||||
import io.circe.generic.auto.*
|
||||
import io.circe.Json
|
||||
import org.http4s.Method.*
|
||||
import org.http4s.client.dsl.io.*
|
||||
import org.http4s.{Header, Request, Status, Uri}
|
||||
import org.http4s.client.Client
|
||||
import org.typelevel.ci.CIStringSyntax
|
||||
import org.http4s.circe.*
|
||||
import java.util.UUID
|
||||
import org.yobble.chat_private_service.config.AppConfig
|
||||
|
||||
case class ProfileClient(client: Client[IO]) {
|
||||
|
||||
def getProfileByUserId(userId: UUID, token: String): IO[Json] = {
|
||||
val uri = Uri.unsafeFromString(s"https://$AppConfig.PROFILE_SERVICE/user_id/$userId")
|
||||
val req = Request[IO](method = GET, uri = uri)
|
||||
.withHeaders(Header.Raw(ci"Authorization", s"Bearer $token"))
|
||||
|
||||
client.fetch(req) {
|
||||
case Status.Successful(resp) => resp.as[Json]
|
||||
case resp => resp.bodyText.compile.string.flatMap(body =>
|
||||
IO.raiseError(new Exception(s"profile_service: ${resp.status.code} $body"))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
def getProfilesByUserIds(userIds: List[UUID], token: String, userId: UUID): IO[Json] = {
|
||||
val uri = Uri.unsafeFromString(s"https://$AppConfig.PROFILE_SERVICE/user_ids/internal")
|
||||
val json = ProfilesByUserIdsRequest(userIds.map(_.toString), userId.toString).asJson
|
||||
|
||||
val req = Request[IO](method = POST, uri = uri)
|
||||
.withHeaders(Header.Raw(ci"Authorization", s"Bearer $token"))
|
||||
.withEntity(json)
|
||||
|
||||
client.fetch(req) {
|
||||
case Status.Successful(resp) => resp.as[Json]
|
||||
case resp => resp.bodyText.compile.string.flatMap(body =>
|
||||
IO.raiseError(new Exception(s"profile_service: ${resp.status.code} $body"))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package org.yobble.module.service.profile
|
||||
|
||||
case class ProfilesByUserIdsRequest(
|
||||
user_ids: List[String],
|
||||
user_id: String
|
||||
)
|
||||
@ -1,7 +1,7 @@
|
||||
package org.yobble.chat_private_service.api.util
|
||||
package org.yobble.module.util
|
||||
|
||||
import org.yobble.module.response.{ErrorDetail, ErrorResponse}
|
||||
import sttp.model.StatusCode
|
||||
import org.yobble.chat_private_service.api.response.{ErrorDetail, ErrorResponse}
|
||||
|
||||
object ErrorUtils {
|
||||
def badRequest(message: String = "Bad request"): ErrorResponse =
|
||||
@ -33,6 +33,7 @@ object ErrorUtils {
|
||||
}
|
||||
|
||||
object ErrorExamples {
|
||||
|
||||
val unauthorized: ErrorResponse = ErrorResponse(
|
||||
status = "error",
|
||||
errors = List(ErrorDetail("login", "Invalid login or password"))
|
||||
Reference in New Issue
Block a user