Compare commits
2 Commits
7762e1de89
...
337c222548
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
337c222548 | ||
|
|
2c63c8af89 |
@ -1,12 +1,12 @@
|
|||||||
package org.yobble.scala_monolith.api.endpoint
|
package org.yobble.scala_monolith.api.endpoint.info
|
||||||
|
|
||||||
import io.circe.generic.auto._
|
import io.circe.generic.auto.*
|
||||||
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
||||||
import sttp.tapir._
|
|
||||||
import sttp.tapir.generic.auto._
|
|
||||||
import sttp.tapir.json.circe._
|
|
||||||
import org.yobble.scala_monolith.api.util.ErrorExamples
|
import org.yobble.scala_monolith.api.util.ErrorExamples
|
||||||
import sttp.model.StatusCode
|
import sttp.model.StatusCode
|
||||||
|
import sttp.tapir.*
|
||||||
|
import sttp.tapir.generic.auto.*
|
||||||
|
import sttp.tapir.json.circe.*
|
||||||
|
|
||||||
object ErrorsEndpoint {
|
object ErrorsEndpoint {
|
||||||
val errorsEndpoint: PublicEndpoint[Unit, ErrorResponse, BaseResponse, Any] =
|
val errorsEndpoint: PublicEndpoint[Unit, ErrorResponse, BaseResponse, Any] =
|
||||||
@ -1,12 +1,12 @@
|
|||||||
package org.yobble.scala_monolith.api.endpoint
|
package org.yobble.scala_monolith.api.endpoint.info
|
||||||
|
|
||||||
import sttp.tapir.*
|
|
||||||
import sttp.tapir.json.circe.jsonBody
|
|
||||||
import sttp.tapir.generic.auto.*
|
|
||||||
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
|
||||||
import io.circe.generic.auto.*
|
import io.circe.generic.auto.*
|
||||||
|
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
||||||
import org.yobble.scala_monolith.api.util.ErrorExamples
|
import org.yobble.scala_monolith.api.util.ErrorExamples
|
||||||
import sttp.model.StatusCode
|
import sttp.model.StatusCode
|
||||||
|
import sttp.tapir.*
|
||||||
|
import sttp.tapir.generic.auto.*
|
||||||
|
import sttp.tapir.json.circe.jsonBody
|
||||||
|
|
||||||
object PingEndpoint {
|
object PingEndpoint {
|
||||||
val pingEndpoint = endpoint
|
val pingEndpoint = endpoint
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package org.yobble.scala_monolith.api.endpoint.info
|
||||||
|
|
||||||
|
import io.circe.generic.auto.*
|
||||||
|
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
||||||
|
import org.yobble.scala_monolith.api.util.ErrorExamples
|
||||||
|
import sttp.model.StatusCode
|
||||||
|
import sttp.tapir.*
|
||||||
|
import sttp.tapir.generic.auto.*
|
||||||
|
import sttp.tapir.json.circe.*
|
||||||
|
|
||||||
|
object TestErrorEndpoint {
|
||||||
|
val testErrorEndpoint: PublicEndpoint[Int, ErrorResponse, Unit, Any] =
|
||||||
|
endpoint.get
|
||||||
|
.in("test-error" / path[Int]("code").description("The HTTP status code to return"))
|
||||||
|
.tags(List("Info"))
|
||||||
|
.name("Test-Error")
|
||||||
|
.summary("Returns a response with the specified error code")
|
||||||
|
.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)),
|
||||||
|
oneOfVariant(StatusCode.ServiceUnavailable, jsonBody[ErrorResponse].description("Service Unavailable").example(ErrorExamples.unavailable))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.out(statusCode(StatusCode.Ok))
|
||||||
|
}
|
||||||
@ -11,6 +11,6 @@ object ErrorResponse {
|
|||||||
implicit val errorDetailEncoder: Encoder[ErrorDetail] = deriveEncoder[ErrorDetail]
|
implicit val errorDetailEncoder: Encoder[ErrorDetail] = deriveEncoder[ErrorDetail]
|
||||||
|
|
||||||
// Encoder без поля code
|
// Encoder без поля code
|
||||||
implicit val errorResponseEncoder: Encoder[ErrorResponse] =
|
// implicit val errorResponseEncoder: Encoder[ErrorResponse] =
|
||||||
deriveEncoder[ErrorResponse].mapJsonObject(_.remove("code"))
|
// deriveEncoder[ErrorResponse].mapJsonObject(_.remove("code"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import cats.effect.IO
|
|||||||
import doobie.Transactor
|
import doobie.Transactor
|
||||||
import org.yobble.scala_monolith.api.endpoint.auth.AuthEndpoints
|
import org.yobble.scala_monolith.api.endpoint.auth.AuthEndpoints
|
||||||
import sttp.tapir.server.ServerEndpoint
|
import sttp.tapir.server.ServerEndpoint
|
||||||
import org.yobble.scala_monolith.api.endpoint.PingEndpoint
|
import org.yobble.scala_monolith.api.endpoint.info.{ErrorsEndpoint, PingEndpoint, TestErrorEndpoint}
|
||||||
import org.yobble.scala_monolith.api.endpoint.ErrorsEndpoint
|
import org.yobble.scala_monolith.api.response.{BaseResponse, ErrorResponse}
|
||||||
import org.yobble.scala_monolith.api.response.BaseResponse
|
|
||||||
import org.yobble.scala_monolith.repository.UserRepositoryImpl
|
import org.yobble.scala_monolith.repository.UserRepositoryImpl
|
||||||
import org.yobble.scala_monolith.service.AuthService
|
import org.yobble.scala_monolith.service.AuthService
|
||||||
|
import sttp.model.StatusCode
|
||||||
|
import org.yobble.scala_monolith.api.util.errorByStatus
|
||||||
|
|
||||||
object AllServerEndpoints {
|
object AllServerEndpoints {
|
||||||
def all(transactor: Transactor[IO]): List[ServerEndpoint[Any, IO]] = {
|
def all(transactor: Transactor[IO]): List[ServerEndpoint[Any, IO]] = {
|
||||||
@ -19,9 +20,12 @@ object AllServerEndpoints {
|
|||||||
AuthEndpoints.loginEndpoint.serverLogic(authService.login)
|
AuthEndpoints.loginEndpoint.serverLogic(authService.login)
|
||||||
)
|
)
|
||||||
|
|
||||||
val otherEndpoints = List(
|
val otherEndpoints: List[ServerEndpoint[Any, IO]] = List(
|
||||||
PingEndpoint.pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse(message = "pong"))),
|
PingEndpoint.pingEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse(message = "pong"))),
|
||||||
ErrorsEndpoint.errorsEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse(message = "errors")))
|
ErrorsEndpoint.errorsEndpoint.serverLogicSuccess(_ => IO.pure(BaseResponse(message = "errors"))),
|
||||||
|
TestErrorEndpoint.testErrorEndpoint.serverLogic(code => {
|
||||||
|
IO.pure(Left(errorByStatus(StatusCode(code))))
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
authEndpoints ++ otherEndpoints
|
authEndpoints ++ otherEndpoints
|
||||||
|
|||||||
@ -5,34 +5,34 @@ import sttp.model.StatusCode
|
|||||||
|
|
||||||
object ErrorUtils {
|
object ErrorUtils {
|
||||||
def badRequest(message: String = "Bad request"): ErrorResponse =
|
def badRequest(message: String = "Bad request"): ErrorResponse =
|
||||||
ErrorResponse(code = 400, errors = List(ErrorDetail("general", message)))
|
ErrorResponse(code = 400, errors = List(ErrorDetail("Bad request", message)))
|
||||||
|
|
||||||
def unauthorized(message: String = "Unauthorized"): ErrorResponse =
|
def unauthorized(message: String = "Unauthorized"): ErrorResponse =
|
||||||
ErrorResponse(code = 401, errors = List(ErrorDetail("login", message)))
|
ErrorResponse(code = 401, errors = List(ErrorDetail("Unauthorized", message)))
|
||||||
|
|
||||||
def forbidden(message: String = "Forbidden"): ErrorResponse =
|
def forbidden(message: String = "Forbidden"): ErrorResponse =
|
||||||
ErrorResponse(code = 403, errors = List(ErrorDetail("permission", message)))
|
ErrorResponse(code = 403, errors = List(ErrorDetail("Forbidden", message)))
|
||||||
|
|
||||||
def notFound(message: String = "Not found"): ErrorResponse =
|
def notFound(message: String = "Not found"): ErrorResponse =
|
||||||
ErrorResponse(code = 404, errors = List(ErrorDetail("resource", message)))
|
ErrorResponse(code = 404, errors = List(ErrorDetail("Not found", message)))
|
||||||
|
|
||||||
def methodNotAllowed(message: String = "Method not allowed"): ErrorResponse =
|
def methodNotAllowed(message: String = "Method not allowed"): ErrorResponse =
|
||||||
ErrorResponse(code = 405, errors = List(ErrorDetail("request", message)))
|
ErrorResponse(code = 405, errors = List(ErrorDetail("Method not allowed", message)))
|
||||||
|
|
||||||
def conflict(message: String = "Conflict"): ErrorResponse =
|
def conflict(message: String = "Conflict"): ErrorResponse =
|
||||||
ErrorResponse(code = 409, errors = List(ErrorDetail("conflict", message)))
|
ErrorResponse(code = 409, errors = List(ErrorDetail("Conflict", message)))
|
||||||
|
|
||||||
def imATeapot(message: String = "This feature is under development"): ErrorResponse =
|
def imATeapot(message: String = "This feature is under development"): ErrorResponse =
|
||||||
ErrorResponse(code = 418, errors = List(ErrorDetail("debug", message)))
|
ErrorResponse(code = 418, errors = List(ErrorDetail("This feature is under development", message)))
|
||||||
|
|
||||||
def validation(errors: List[(String, String)]): ErrorResponse =
|
def validation(errors: List[(String, String)]): ErrorResponse =
|
||||||
ErrorResponse(code = 422, errors = errors.map((ErrorDetail.apply _).tupled))
|
ErrorResponse(code = 422, errors = errors.map((ErrorDetail.apply _).tupled))
|
||||||
|
|
||||||
def internalServerError(message: String = "Internal Server Error"): ErrorResponse =
|
def internalServerError(message: String = "Internal Server Error"): ErrorResponse =
|
||||||
ErrorResponse(code = 500, errors = List(ErrorDetail("server", message)))
|
ErrorResponse(code = 500, errors = List(ErrorDetail("Internal Server Error", message)))
|
||||||
|
|
||||||
def serviceUnavailableError(message: String = "Service unavailable"): ErrorResponse =
|
def serviceUnavailableError(message: String = "Service unavailable"): ErrorResponse =
|
||||||
ErrorResponse(code = 503, errors = List(ErrorDetail("proxy", message)))
|
ErrorResponse(code = 503, errors = List(ErrorDetail("Service unavailable", message)))
|
||||||
}
|
}
|
||||||
|
|
||||||
object ErrorExamples {
|
object ErrorExamples {
|
||||||
@ -40,37 +40,37 @@ object ErrorExamples {
|
|||||||
val badRequest: ErrorResponse =
|
val badRequest: ErrorResponse =
|
||||||
ErrorResponse(
|
ErrorResponse(
|
||||||
code = 400,
|
code = 400,
|
||||||
errors = List(ErrorDetail("general", "Bad request syntax or invalid parameters"))
|
errors = List(ErrorDetail("Bad request", "Bad request syntax or invalid parameters"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val unauthorized: ErrorResponse = ErrorResponse(
|
val unauthorized: ErrorResponse = ErrorResponse(
|
||||||
code = 401,
|
code = 401,
|
||||||
errors = List(ErrorDetail("login", "Invalid login or password"))
|
errors = List(ErrorDetail("Unauthorized", "Invalid login or password"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val forbidden: ErrorResponse = ErrorResponse(
|
val forbidden: ErrorResponse = ErrorResponse(
|
||||||
code = 403,
|
code = 403,
|
||||||
errors = List(ErrorDetail("permission", "You don't have access to this resource"))
|
errors = List(ErrorDetail("Forbidden", "You don't have access to this resource"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val notFound: ErrorResponse = ErrorResponse(
|
val notFound: ErrorResponse = ErrorResponse(
|
||||||
code = 404,
|
code = 404,
|
||||||
errors = List(ErrorDetail("resource", "Requested resource not found"))
|
errors = List(ErrorDetail("Not found", "Requested resource not found"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val notAllowed: ErrorResponse = ErrorResponse(
|
val notAllowed: ErrorResponse = ErrorResponse(
|
||||||
code = 405,
|
code = 405,
|
||||||
errors = List(ErrorDetail("request", "Method not allowed on this endpoint"))
|
errors = List(ErrorDetail("Method not allowed", "Method not allowed on this endpoint"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val conflict: ErrorResponse = ErrorResponse(
|
val conflict: ErrorResponse = ErrorResponse(
|
||||||
code = 409,
|
code = 409,
|
||||||
errors = List(ErrorDetail("conflict", "Resource already exists or conflict occurred"))
|
errors = List(ErrorDetail("Conflict", "Resource already exists or conflict occurred"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val teapot: ErrorResponse = ErrorResponse(
|
val teapot: ErrorResponse = ErrorResponse(
|
||||||
code = 418,
|
code = 418,
|
||||||
errors = List(ErrorDetail("debug", "This feature is under development"))
|
errors = List(ErrorDetail("This feature is under development", "This feature is under development"))
|
||||||
)
|
)
|
||||||
|
|
||||||
val validation: ErrorResponse = ErrorResponse(
|
val validation: ErrorResponse = ErrorResponse(
|
||||||
@ -80,12 +80,12 @@ object ErrorExamples {
|
|||||||
|
|
||||||
val internal: ErrorResponse = ErrorResponse(
|
val internal: ErrorResponse = ErrorResponse(
|
||||||
code = 500,
|
code = 500,
|
||||||
errors = List(ErrorDetail("server", "An unexpected error occurred. Please try again later."))
|
errors = List(ErrorDetail("Internal Server Error", "An unexpected error occurred. Please try again later."))
|
||||||
)
|
)
|
||||||
|
|
||||||
val unavailable: ErrorResponse = ErrorResponse(
|
val unavailable: ErrorResponse = ErrorResponse(
|
||||||
code = 503,
|
code = 503,
|
||||||
errors = List(ErrorDetail("proxy", "Service unavailable."))
|
errors = List(ErrorDetail("Service unavailable", "Service unavailable."))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import doobie.Read
|
|||||||
import doobie.util.meta.Meta
|
import doobie.util.meta.Meta
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
case class User(id: UUID, login: String, passwordHash: String) derives Read
|
case class User(id: UUID, login: String, passwordHash: String, isBlocked: Boolean, isDeleted: Boolean) derives Read
|
||||||
|
|
||||||
object User {
|
object User {
|
||||||
implicit val uuidMeta: Meta[UUID] = Meta[String].timap(UUID.fromString)(_.toString)
|
implicit val uuidMeta: Meta[UUID] = Meta[String].timap(UUID.fromString)(_.toString)
|
||||||
|
|||||||
@ -12,7 +12,7 @@ trait UserRepository {
|
|||||||
|
|
||||||
class UserRepositoryImpl(transactor: Transactor[IO]) extends UserRepository {
|
class UserRepositoryImpl(transactor: Transactor[IO]) extends UserRepository {
|
||||||
override def findByLogin(login: String): IO[Option[User]] = {
|
override def findByLogin(login: String): IO[Option[User]] = {
|
||||||
sql"SELECT id, login, password_hash as passwordHash FROM users WHERE login = $login"
|
sql"SELECT id, login, password_hash as passwordHash, is_blocked as isBlocked, is_deleted as isDeleted FROM users WHERE login = $login"
|
||||||
.query[User]
|
.query[User]
|
||||||
.option
|
.option
|
||||||
.transact(transactor)
|
.transact(transactor)
|
||||||
|
|||||||
@ -8,11 +8,17 @@ class AuthService(userRepository: UserRepository) {
|
|||||||
|
|
||||||
def login(request: LoginRequest): IO[Either[String, LoginResponse]] = {
|
def login(request: LoginRequest): IO[Either[String, LoginResponse]] = {
|
||||||
userRepository.findByLogin(request.login).map {
|
userRepository.findByLogin(request.login).map {
|
||||||
case Some(user) if user.passwordHash == request.password =>
|
case Some(user) if user.passwordHash != request.password =>
|
||||||
|
Left("Invalid login or password")
|
||||||
|
case Some(user) if user.isBlocked =>
|
||||||
|
Left("User account is disabled")
|
||||||
|
case Some(user) if user.isDeleted =>
|
||||||
|
Left("User account is deleted")
|
||||||
|
case Some(user) =>
|
||||||
// TODO: Implement proper password hashing (e.g., with bcrypt)
|
// TODO: Implement proper password hashing (e.g., with bcrypt)
|
||||||
// TODO: Implement real token generation
|
// TODO: Implement real token generation
|
||||||
Right(LoginResponse(accessToken = "fake-access-token", refreshToken = "fake-refresh-token"))
|
Right(LoginResponse(accessToken = "fake-access-token", refreshToken = "fake-refresh-token"))
|
||||||
case _ =>
|
case None =>
|
||||||
Left("Invalid login or password")
|
Left("Invalid login or password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user