Support JWT
This commit is contained in:
parent
25318cd6de
commit
aa9d60e4d1
9 changed files with 96 additions and 19 deletions
|
|
@ -16,6 +16,8 @@ import zio.logging.LogFormat.*
|
|||
import zio.logging.consoleLogger
|
||||
import zio.schema.codec.JsonCodec.ExplicitConfig
|
||||
|
||||
import lu.foyer.auth.AuthMiddleware.jwtAuthentication
|
||||
import lu.foyer.auth.JwtScalaTokenService
|
||||
import lu.foyer.clients.*
|
||||
import lu.foyer.contracts.*
|
||||
|
||||
|
|
@ -29,6 +31,7 @@ object HttpServer:
|
|||
contract <- ZIO.service[ContractController]
|
||||
openAPI = OpenAPIGen.fromEndpoints(client.endpoints ++ contract.endpoints)
|
||||
yield (client.routes ++ contract.routes)
|
||||
@@ jwtAuthentication("Insurance")
|
||||
@@ cors(corsConfig) @@ Middleware.debug
|
||||
++ SwaggerUI.routes("docs" / "openapi", openAPI)
|
||||
|
||||
|
|
@ -52,10 +55,12 @@ object App extends ZIOAppDefault:
|
|||
val app =
|
||||
for
|
||||
routes <- HttpServer.routes
|
||||
server <- Server.serve(routes).provide(Server.default)
|
||||
server <- Server.serve(routes)
|
||||
yield server
|
||||
|
||||
override def run = app.provide(
|
||||
Server.default,
|
||||
JwtScalaTokenService.live,
|
||||
CommandEngine.layer[ClientCommand, ClientEvent, ClientState],
|
||||
CommandEngine.layer[ContractCommand, ContractEvent, ContractState],
|
||||
ClientHandlers.layer,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import zio.http.codec.PathCodec.path
|
|||
import zio.http.endpoint.*
|
||||
import zio.schema.*
|
||||
|
||||
import lu.foyer.auth.UserInfo
|
||||
|
||||
trait CommandEngineController[Command, Event: Schema, State: Schema] extends JsonApiController:
|
||||
|
||||
def commandEngine: CommandEngine[Command, Event, State]
|
||||
|
|
@ -36,12 +38,12 @@ trait CommandEngineController[Command, Event: Schema, State: Schema] extends Jso
|
|||
|
||||
private def generateCreateCommand(handler: CommandHandler[Command, Event, State]) =
|
||||
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
||||
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
||||
val endpoint = Endpoint(Method.POST / entityName / "commands" / handler.name)
|
||||
.in[Command]
|
||||
.jsonApiOneWithStatus[Event](Status.Created)
|
||||
val route = endpoint.implementJsonApiOneEvent(command =>
|
||||
for
|
||||
entityId <- Random.nextUUID
|
||||
entityId <- Random.nextUUID
|
||||
(event, _) <- commandEngine
|
||||
.handleCommand(command, handler.name, entityId.toString)
|
||||
yield Some(event)
|
||||
|
|
@ -50,7 +52,7 @@ trait CommandEngineController[Command, Event: Schema, State: Schema] extends Jso
|
|||
|
||||
private def generateUpdateCommand(handler: CommandHandler[Command, Event, State]) =
|
||||
given Schema[Command] = handler.commandSchema.asInstanceOf[Schema[Command]]
|
||||
val endpoint = Endpoint(
|
||||
val endpoint = Endpoint(
|
||||
Method.PUT / entityName / string("entityId") / "commands" / handler.name
|
||||
)
|
||||
.in[Command]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import zio.schema.*
|
|||
|
||||
import lu.foyer.JsonApiResponse.Many
|
||||
import lu.foyer.JsonApiResponse.One
|
||||
import lu.foyer.auth.UserInfo
|
||||
|
||||
object JsonApiResponse:
|
||||
|
||||
|
|
@ -89,7 +90,7 @@ trait JsonApiController:
|
|||
endpoint: Endpoint[PathInput, Input, JsonApiError, One[Output], AuthType.None]
|
||||
)
|
||||
def implementJsonApiOne[Env, A](
|
||||
f: Input => ZIO[Env, JsonApiError | Throwable, Option[A]],
|
||||
f: Input => ZIO[Env & UserInfo, JsonApiError | Throwable, Option[A]],
|
||||
getId: A => String,
|
||||
getEntity: A => Output,
|
||||
onthology: String = this.onthology,
|
||||
|
|
@ -99,7 +100,7 @@ trait JsonApiController:
|
|||
_: ProxyHeaders
|
||||
) => None
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
endpoint.implement(input =>
|
||||
for
|
||||
item <- f(input)
|
||||
|
|
@ -127,7 +128,7 @@ trait JsonApiController:
|
|||
def implementJsonApiOneEntity[Env](
|
||||
f: Input => RIO[Env, Option[Entity[Output]]]
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
implementJsonApiOne(
|
||||
f,
|
||||
_.entityId,
|
||||
|
|
@ -140,9 +141,9 @@ trait JsonApiController:
|
|||
)
|
||||
|
||||
def implementJsonApiOneEvent[Env](
|
||||
f: Input => ZIO[Env, JsonApiError | Throwable, Option[Event[Output]]]
|
||||
f: Input => ZIO[Env & UserInfo, JsonApiError | Throwable, Option[Event[Output]]]
|
||||
)(implicit trace: Trace
|
||||
): Route[Env & ProxyHeaders, Nothing] =
|
||||
): Route[Env & ProxyHeaders & UserInfo, Nothing] =
|
||||
implementJsonApiOne(
|
||||
f,
|
||||
_.eventId,
|
||||
|
|
|
|||
25
api/src/lu/foyer/auth/AuthMiddleware.scala
Normal file
25
api/src/lu/foyer/auth/AuthMiddleware.scala
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
import zio.*
|
||||
import zio.http.*
|
||||
|
||||
object AuthMiddleware:
|
||||
def jwtAuthentication(realm: String): HandlerAspect[JwtTokenService, UserInfo] =
|
||||
HandlerAspect.interceptIncomingHandler {
|
||||
handler { (request: Request) =>
|
||||
request.header(Header.Authorization) match
|
||||
case Some(Header.Authorization.Bearer(token)) =>
|
||||
ZIO
|
||||
.serviceWithZIO[JwtTokenService](_.verify(token.value.asString))
|
||||
.map(UserInfo(_))
|
||||
.map(userInfo => (request, userInfo))
|
||||
.orElseFail(
|
||||
Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm)))
|
||||
)
|
||||
case _ =>
|
||||
ZIO.fail(
|
||||
Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm)))
|
||||
)
|
||||
}
|
||||
}
|
||||
29
api/src/lu/foyer/auth/JwtScalaTokenService.scala
Normal file
29
api/src/lu/foyer/auth/JwtScalaTokenService.scala
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
import java.time.Clock
|
||||
import java.util.Base64
|
||||
|
||||
import zio.*
|
||||
import zio.json.*
|
||||
|
||||
import lu.foyer.auth.JwtScalaTokenService.Claims
|
||||
|
||||
case class JwtScalaTokenService() extends JwtTokenService:
|
||||
implicit val clock: Clock = Clock.systemUTC
|
||||
|
||||
// Doesn't actually verify, we just pretend for simplicity
|
||||
override def verify(token: String): Task[String] =
|
||||
token.split('.').drop(1).headOption match
|
||||
case Some(base64) =>
|
||||
new String(Base64.getDecoder().decode(base64)).fromJson[Claims] match
|
||||
case Right(claims) => ZIO.succeed(claims.sub)
|
||||
case Left(error) =>
|
||||
ZIO.logWarning(s"Failed to parse JWT claims : $error") *>
|
||||
ZIO.fail(new Exception("Invalid token"))
|
||||
case None => ZIO.fail(new Exception("Invalid token"))
|
||||
|
||||
object JwtScalaTokenService:
|
||||
val live = ZLayer.succeed(JwtScalaTokenService())
|
||||
|
||||
final private[auth] case class Claims(sub: String) derives JsonDecoder
|
||||
|
|
@ -3,6 +3,8 @@ package lu.foyer
|
|||
import zio.*
|
||||
import zio.schema.Schema
|
||||
|
||||
import lu.foyer.auth.UserInfo
|
||||
|
||||
final case class Entity[T](entityId: String, data: T, version: Long)
|
||||
final case class Event[T](entityId: String, data: T, eventId: String)
|
||||
|
||||
|
|
@ -28,13 +30,14 @@ trait CommandHandler[+Command, +Event, +State]:
|
|||
def commandSchema: Schema[?]
|
||||
|
||||
trait CommandHandlerCreate[Command: Schema, Event] extends CommandHandler[Command, Event, Nothing]:
|
||||
def onCommand(entityId: String, command: Command): IO[JsonApiError | Throwable, Event]
|
||||
def onCommand(entityId: String, command: Command): ZIO[UserInfo, JsonApiError | Throwable, Event]
|
||||
val isCreate = true
|
||||
val commandSchema = summon[Schema[Command]]
|
||||
|
||||
trait CommandHandlerUpdate[Command: Schema, Event, State]
|
||||
extends CommandHandler[Command, Event, State]:
|
||||
def onCommand(entityId: String, state: State, command: Command) : IO[JsonApiError | Throwable, Event]
|
||||
def onCommand(entityId: String, state: State, command: Command)
|
||||
: ZIO[UserInfo, JsonApiError | Throwable, Event]
|
||||
val isCreate = false
|
||||
val commandSchema = summon[Schema[Command]]
|
||||
|
||||
|
|
@ -45,7 +48,7 @@ class CommandEngine[Command, Event, State](
|
|||
val stateRepo: StateRepository[State]):
|
||||
|
||||
def handleCommand(command: Command, name: String, entityId: String)
|
||||
: IO[JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
||||
: ZIO[UserInfo, JsonApiError | Throwable, (lu.foyer.Event[Event], lu.foyer.Entity[State])] =
|
||||
for
|
||||
handler <- ZIO
|
||||
.succeed(handlers.find(_.name == name))
|
||||
|
|
@ -70,7 +73,7 @@ class CommandEngine[Command, Event, State](
|
|||
entityId: String,
|
||||
entityOption: Option[Entity[State]],
|
||||
handler: CommandHandler[Command, Event, State]
|
||||
): IO[JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
|
||||
): ZIO[UserInfo, JsonApiError | Throwable, (Event, Option[State])] = (entityOption, handler) match
|
||||
case (None, h) if !h.isUpdate =>
|
||||
h.asInstanceOf[CommandHandlerCreate[Command, Event]]
|
||||
.onCommand(entityId, command)
|
||||
|
|
|
|||
7
core/src/lu/foyer/auth/JwtTokenService.scala
Normal file
7
core/src/lu/foyer/auth/JwtTokenService.scala
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
import zio.*
|
||||
|
||||
trait JwtTokenService:
|
||||
def verify(token: String): Task[String]
|
||||
4
core/src/lu/foyer/auth/UserInfo.scala
Normal file
4
core/src/lu/foyer/auth/UserInfo.scala
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package lu.foyer
|
||||
package auth
|
||||
|
||||
final case class UserInfo(subject: String)
|
||||
|
|
@ -3,6 +3,7 @@ package contracts
|
|||
|
||||
import zio.*
|
||||
|
||||
import lu.foyer.auth.UserInfo
|
||||
import lu.foyer.clients.*
|
||||
|
||||
object ContractHandlers:
|
||||
|
|
@ -92,11 +93,11 @@ class ApproveHandler(employeeService: EmployeeService)
|
|||
val name = "approve"
|
||||
|
||||
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Approve)
|
||||
: Task[ContractEvent.Approved] =
|
||||
: RIO[UserInfo, ContractEvent.Approved] =
|
||||
for
|
||||
user <- ZIO.succeed("") // TODO current user
|
||||
user <- ZIO.service[UserInfo]
|
||||
employee <- ZIO
|
||||
.fromEither(employeeService.fetchOne(user))
|
||||
.fromEither(employeeService.fetchOne(user.subject))
|
||||
.mapError(new IllegalArgumentException(_))
|
||||
yield ContractEvent.Approved(employee)
|
||||
|
||||
|
|
@ -110,11 +111,11 @@ class RejectHandler(employeeService: EmployeeService)
|
|||
val name = "reject"
|
||||
|
||||
def onCommand(entityId: String, state: ContractState.Pending, command: ContractCommand.Reject)
|
||||
: Task[ContractEvent.Rejected] =
|
||||
: RIO[UserInfo, ContractEvent.Rejected] =
|
||||
for
|
||||
user <- ZIO.succeed("") // TODO current user
|
||||
user <- ZIO.service[UserInfo]
|
||||
employee <- ZIO
|
||||
.fromEither(employeeService.fetchOne(user))
|
||||
.fromEither(employeeService.fetchOne(user.subject))
|
||||
.mapError(new IllegalArgumentException(_))
|
||||
yield ContractEvent.Rejected(employee, command.comment)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue