├── .gitignore ├── LICENSE ├── README.md ├── activator.properties ├── auth-codecard ├── project │ └── plugins.sbt └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ ├── AuthCodeCard.scala │ ├── Config.scala │ ├── Gateway.scala │ ├── JsonProtocols.scala │ ├── Repository.scala │ └── Service.scala ├── auth-fb └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ ├── AuthFb.scala │ ├── Config.scala │ ├── Gateway.scala │ ├── JsonProtocols.scala │ └── Service.scala ├── auth-password └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ ├── AuthPassword.scala │ ├── Config.scala │ ├── Gateway.scala │ ├── JsonProtocols.scala │ ├── Repository.scala │ └── Service.scala ├── btc-common └── src │ └── main │ └── scala │ └── btc │ └── common │ └── Messages.scala ├── btc-users └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ ├── DataFetcher.scala │ ├── Main.scala │ ├── UserHandler.scala │ └── UsersManager.scala ├── btc-ws ├── app │ └── controllers │ │ └── BtcWs.scala └── conf │ ├── application.conf │ └── routes ├── build.sbt ├── docker-compose.yml ├── identity-manager └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ └── IdentityManager.scala ├── metrics-collector ├── app │ ├── controllers │ │ └── MetricsCollector.scala │ └── global │ │ └── FlowInitializer.scala └── conf │ ├── application.conf │ └── routes ├── metrics-common └── src │ └── main │ └── scala │ └── metrics │ └── common │ ├── Metrics.scala │ └── MetricsDirectives.scala ├── postgres ├── Dockerfile ├── auth_entry.sql ├── identity.sql └── init.sql ├── project ├── build.properties └── plugins.sbt ├── session-manager └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ └── SessionManager.scala ├── token-manager └── src │ └── main │ ├── resources │ └── application.conf │ └── scala │ ├── Config.scala │ ├── JsonProtocols.scala │ ├── Repository.scala │ ├── Service.scala │ └── TokenManager.scala └── tutorial ├── arch.png ├── btc-ws.png ├── flow.png └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | # IntelliJ specific 20 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Iterators 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive microservices 2 | 3 | [![Join the chat at https://gitter.im/theiterators/reactive-microservices](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/theiterators/reactive-microservices?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | Reactive microservices is an Typesafe Activator Template completely devoted to microservices architecture. It lets you learn about microservices in general - different patterns, communication protocols and 'tastes' of microservices. All these concepts are demonstrated using Scala, Akka, Play and other tools from Scala ecosystem. For the sake of clarity, we skipped topics related to deployment and operations. 6 | 7 | ## Prerequisites 8 | 9 | To feel comfortable while playing with this template, make sure you know basics of Akka HTTP which is a cornerstone of this project. We recently released an [Akka HTTP activator template](https://typesafe.com/activator/template/akka-http-microservice) that may help you start. At least brief knowledge of [Akka remoting](https://typesafe.com/activator/template/akka-sample-remote-scala), [Akka persistence](https://typesafe.com/activator/template/akka-sample-persistence-scala), [Akka streams](https://typesafe.com/activator/template/akka-stream-scala) and [Play Framework websockets](https://typesafe.com/activator/template/anonymous-chat) is also highly recommended. 10 | 11 | ## Structure 12 | 13 | This activator template consists of 9 runnable subprojects — the microservices: 14 | * auth ones: 15 | * `auth-codecard` 16 | * `auth-fb` 17 | * `auth-password` 18 | * `identity-manager` 19 | * `session-manager` 20 | * `token-manager` 21 | * business logic ones: 22 | * `btc-users` 23 | * `btc-ws` 24 | * miscellaneous ones: 25 | * `metrics-collector` 26 | 27 | They uses different communication methods, different databases, and different frameworks. 28 | 29 | ## Setup 30 | 31 | #### Review the configuration files 32 | 33 | Take some time to review `application.conf` files that are located in ```resource``` subdirectory of each microservice. You can also look at `docker-compose.yml` file, which contains docker preconfigurated images for all the required databases. 34 | 35 | #### Run migrations (You don't need to do this step if you want to use our docker container) 36 | 37 | For `auth-codecard`, `identity-manager` and `auth-password` you need to run the SQL migration scripts which are located in `postgres` directory. If you want to use non-default names please tweak the `application.conf` files. 38 | You can also tweak and use this script in your console. 39 | 40 | ``` 41 | cd /where/this/activator/template/is/located/ 42 | psql -h localhost -U postgres -f ./postgres/init.sql && 43 | psql -h localhost -U postgres -f ./postgres/auth_entry.sql && 44 | psql -h localhost -U postgres -f ./postgres/identity.sql 45 | ``` 46 | 47 | ## Running 48 | 49 | Run `docker-compose up` in project main directory to launch databases, or if you are using your own database instances, make sure you have PostgreSQL, MongoDB and Redis up and running. 50 | 51 | #### akka-http 52 | You can run each service separately, but we also we provided a SBT task called `runAll`. 53 | 54 | #### Play 55 | Due to some issues with Play/sbt cooperation `metrics-collector` and `btc-ws` should be run separately. 56 | In order to run them in one sbt CLI instance use these commands: 57 | ``` 58 | ; project btc-ws; run 9000 59 | ``` 60 | ``` 61 | ; project metrics-collector; run 5001 62 | ``` 63 | 64 | Everything else should work out of the box. Enjoy! 65 | 66 | ## Author & license 67 | 68 | If you have any questions regarding this project contact: 69 | 70 | Łukasz Sowa from [Iterators](https://www.iteratorshq.com). 71 | 72 | For licensing info see LICENSE file in project's root directory. 73 | -------------------------------------------------------------------------------- /activator.properties: -------------------------------------------------------------------------------- 1 | name=reactive-microservices 2 | title=Microservices - reactive communication patterns 3 | description=Project showcasing different microservice communication styles using Scala, Akka, Play and other tools from Scala ecosystem. 4 | tags=akka,akka-http,akka-persistence,playframework,slick,reactive-mongo,rediscala,reactive-platform,reactive,streams,scala,microservice,authentication 5 | authorName=Iterators 6 | authorLink=https://iterato.rs 7 | authorLogo=https://iterato.rs/img/iterators-activator-logo.png 8 | authorBio=Iterators deliver software development consulting services worldwide. With broad experience using Typesafe Reactive Platform, Iterators provide their clients with enterprise content management systems and consumer solutions with strong focus on scalability and data instrumentation. 9 | authorTwitter=@iteratorshq -------------------------------------------------------------------------------- /auth-codecard/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") 2 | -------------------------------------------------------------------------------- /auth-codecard/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8005 8 | } 9 | 10 | services { 11 | identity-manager { 12 | host = "localhost" 13 | port = 8000 14 | } 15 | token-manager { 16 | host = "localhost" 17 | port = 8010 18 | } 19 | } 20 | 21 | db { 22 | url = "jdbc:postgresql://localhost:5432/auth_codecard" 23 | user = "postgres" 24 | password = "postgres" 25 | } 26 | 27 | auth-codecard { 28 | cardSize = 20 29 | code { 30 | active-time = 300000 // 5 minutes 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/AuthCodeCard.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 4 | import akka.http.scaladsl.marshalling.ToResponseMarshallable 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.stream.ActorFlowMaterializer 8 | 9 | case class CodeCard(id: Long, codes: Seq[String], userIdentifier: String) 10 | case class RegisterResponse(identity: Identity, codesCard: CodeCard) 11 | case class LoginRequest(userIdentifier: String, cardIndex: Long, codeIndex: Long, code: String) 12 | case class ActivateCodeRequest(userIdentifier: String) 13 | case class ActivateCodeResponse(cardIndex: Long, codeIndex: Long) 14 | case class GetCodeCardRequest(userIdentifier: String) 15 | case class GetCodeCardResponse(userIdentifier: String, codesCard: CodeCard) 16 | 17 | case class Identity(id: Long) 18 | case class Token(value: String, validTo: Long, identityId: Long, authMethods: Set[String]) 19 | 20 | object AuthCodeCardCard extends App with JsonProtocols with Config { 21 | implicit val actorSystem = ActorSystem() 22 | implicit val materializer = ActorFlowMaterializer() 23 | implicit val dispatcher = actorSystem.dispatcher 24 | 25 | val repository = new Repository 26 | val gateway = new Gateway 27 | val service = new Service(gateway, repository) 28 | 29 | Http().bindAndHandle(interface = interface, port = port, handler = { 30 | logRequestResult("auth-codecard") { 31 | (path("register" / "codecard" ) & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token")) { (tokenValue) => 32 | complete { 33 | service.register(tokenValue).map[ToResponseMarshallable] { 34 | case Right(response) => Created -> response 35 | case Left(errorMessage) => BadRequest -> errorMessage 36 | } 37 | } 38 | } ~ 39 | (path("login" / "codecard" / "activate") & pathEndOrSingleSlash & post & entity(as[ActivateCodeRequest])) { (request) => 40 | complete { 41 | service.activateCode(request).map[ToResponseMarshallable] { 42 | case Right(response) => OK -> response 43 | case Left(errorMessage) => BadRequest -> errorMessage 44 | } 45 | } 46 | } ~ 47 | (path("login" / "codecard") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[LoginRequest])) { (tokenValue, request) => 48 | complete { 49 | service.login(request, tokenValue).map[ToResponseMarshallable] { 50 | case Right(response) => Created -> response 51 | case Left(errorMessage) => BadRequest -> errorMessage 52 | } 53 | } 54 | } ~ 55 | (path("generate" / "codecard") & pathEndOrSingleSlash & post & optionalHeaderValueByName("Auth-Token") & entity(as[GetCodeCardRequest])) { (tokenValue, request) => 56 | complete { 57 | service.getCodeCard(request, tokenValue).map[ToResponseMarshallable] { 58 | case Right(response) => OK -> response 59 | case Left(errorMessage) => BadRequest -> errorMessage 60 | } 61 | } 62 | } 63 | } 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/Config.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.config.ConfigFactory 2 | 3 | trait Config { 4 | protected val config = ConfigFactory.load() 5 | protected val interface = config.getString("http.interface") 6 | protected val port = config.getInt("http.port") 7 | protected val dbUrl = config.getString("db.url") 8 | protected val dbUser = config.getString("db.user") 9 | protected val dbPassword = config.getString("db.password") 10 | 11 | protected val identityManagerHost = config.getString("services.identity-manager.host") 12 | protected val identityManagerPort = config.getInt("services.identity-manager.port") 13 | protected val tokenManagerHost = config.getString("services.token-manager.host") 14 | protected val tokenManagerPort = config.getInt("services.token-manager.port") 15 | 16 | protected val cardSize = config.getInt("auth-codecard.cardSize") 17 | protected val codeActiveTime = config.getInt("auth-codecard.code.active-time") 18 | } 19 | -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/Gateway.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.client.RequestBuilding 4 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 7 | import akka.http.scaladsl.unmarshalling.Unmarshal 8 | import akka.stream.FlowMaterializer 9 | import akka.stream.scaladsl.{Sink, Source} 10 | import java.io.IOException 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | case class InternalLoginRequest(identityId: Long, authMethod: String = "codecard") 14 | case class InternalReloginRequest(tokenValue: String, authMethod: String = "codecard") 15 | 16 | class Gateway(implicit actorSystem: ActorSystem, materializer: FlowMaterializer, ec: ExecutionContext) 17 | extends JsonProtocols with Config { 18 | 19 | private val identityManagerConnectionFlow = Http().outgoingConnection(identityManagerHost, identityManagerPort) 20 | private val tokenManagerConnectionFlow = Http().outgoingConnection(tokenManagerHost, tokenManagerPort) 21 | 22 | private def requestIdentityManager(request: HttpRequest): Future[HttpResponse] = { 23 | Source.single(request).via(identityManagerConnectionFlow).runWith(Sink.head) 24 | } 25 | 26 | private def requestTokenManager(request: HttpRequest): Future[HttpResponse] = { 27 | Source.single(request).via(tokenManagerConnectionFlow).runWith(Sink.head) 28 | } 29 | 30 | def requestToken(tokenValue: String): Future[Either[String, Token]] = { 31 | requestTokenManager(RequestBuilding.Get(s"/tokens/$tokenValue")).flatMap { response => 32 | response.status match { 33 | case Success(_) => Unmarshal(response.entity).to[Token].map(Right(_)) 34 | case NotFound => Future.successful(Left("Token expired or not found")) 35 | case _ => Future.failed(new IOException(s"Token request failed with status ${response.status} and error ${response.entity}")) 36 | } 37 | } 38 | } 39 | 40 | def requestNewIdentity(): Future[Identity] = { 41 | requestIdentityManager(RequestBuilding.Post("/identities")).flatMap { response => 42 | response.status match { 43 | case Success(_) => Unmarshal(response.entity).to[Identity] 44 | case _ => Future.failed(new IOException(s"Identity request failed with status ${response.status} and error ${response.entity}")) 45 | } 46 | } 47 | } 48 | 49 | def requestLogin(identityId: Long): Future[Token] = { 50 | val loginRequest = InternalLoginRequest(identityId) 51 | requestTokenManager(RequestBuilding.Post("/tokens", loginRequest)).flatMap { response => 52 | response.status match { 53 | case Success(_) => Unmarshal(response.entity).to[Token] 54 | case _ => Future.failed(new IOException(s"Login request failed with status ${response.status} and error ${response.entity}")) 55 | } 56 | } 57 | } 58 | 59 | def requestRelogin(tokenValue: String): Future[Option[Token]] = { 60 | requestTokenManager(RequestBuilding.Patch("/tokens", InternalReloginRequest(tokenValue))).flatMap { response => 61 | response.status match { 62 | case Success(_) => Unmarshal(response.entity).to[Token].map(Option(_)) 63 | case NotFound => Future.successful(None) 64 | case _ => Future.failed(new IOException(s"Relogin request failed with status ${response.status} and error ${response.entity}")) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/JsonProtocols.scala: -------------------------------------------------------------------------------- 1 | import spray.json.DefaultJsonProtocol 2 | 3 | trait JsonProtocols extends DefaultJsonProtocol { 4 | protected implicit val identityFormat = jsonFormat1(Identity) 5 | protected implicit val tokenFormat = jsonFormat4(Token) 6 | protected implicit val codeCardFormat = jsonFormat3(CodeCard) 7 | protected implicit val authEntryFormat = jsonFormat4(AuthEntry) 8 | protected implicit val internalLoginRequestFormat = jsonFormat2(InternalLoginRequest) 9 | protected implicit val internalReloginRequestFormat = jsonFormat2(InternalReloginRequest) 10 | protected implicit val registerResponseFormat = jsonFormat2(RegisterResponse) 11 | protected implicit val loginRequestFormat = jsonFormat4(LoginRequest) 12 | protected implicit val activateCodeRequestFormat = jsonFormat1(ActivateCodeRequest) 13 | protected implicit val activateCodeResponseFormat = jsonFormat2(ActivateCodeResponse) 14 | protected implicit val getCodeCardRequestFormat = jsonFormat1(GetCodeCardRequest) 15 | protected implicit val getCodeCardResponseFormat = jsonFormat2(GetCodeCardResponse) 16 | } 17 | -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/Repository.scala: -------------------------------------------------------------------------------- 1 | import scala.slick.lifted.{ProvenShape, Tag} 2 | import scala.slick.driver.PostgresDriver.simple._ 3 | import scala.concurrent._ 4 | 5 | case class AuthEntry(userIdentifier: String, identityId: Long, createdAt: Long, lastCard: Long) 6 | 7 | case class Code(userIdentifier: String, cardIndex: Long, codeIndex: Long, code: String, createdAt: Long, activatedAt: Option[Long] = None, usedAt: Option[Long] = None) 8 | 9 | class AuthEntries(tag: Tag) extends Table[AuthEntry](tag, "auth_entry") { 10 | def userIdentifier = column[String]("user_identifier", O.PrimaryKey, O.NotNull) 11 | def identityId = column[Long]("identity_id", O.NotNull) 12 | def createdAt = column[Long]("created_at", O.NotNull) 13 | def lastCard = column[Long]("last_card") 14 | override def * : ProvenShape[AuthEntry] = (userIdentifier, identityId, createdAt, lastCard) <> (AuthEntry.tupled, AuthEntry.unapply) 15 | } 16 | 17 | class Codes(tag: Tag) extends Table[Code](tag, "code") { 18 | def userIdentifier = column[String]("user_identifier", O.NotNull) 19 | def cardIndex = column[Long]("card_index", O.NotNull) 20 | def codeIndex = column[Long]("code_index", O.NotNull) 21 | def code = column[String]("code", O.NotNull) 22 | def createdAt = column[Long]("created_at", O.NotNull) 23 | def activatedAt = column[Option[Long]]("activated_at") 24 | def usedAt = column[Option[Long]]("used_at") 25 | override def * : ProvenShape[Code] = (userIdentifier, cardIndex, codeIndex, code, createdAt, activatedAt, usedAt) <> (Code.tupled, Code.unapply) 26 | } 27 | 28 | class Repository extends Config { 29 | def useCode(userIdentifier: String, cardIdx: Long, codeIdx: Long, code: String): Int = { 30 | blocking { 31 | db.withSession { implicit s => 32 | codesQuery.filter(codeQ => codeQ.userIdentifier === userIdentifier && 33 | codeQ.cardIndex === cardIdx && 34 | codeQ.codeIndex === codeIdx && 35 | codeQ.code === code && 36 | codeQ.usedAt.isEmpty === true && 37 | codeQ.activatedAt >= (System.currentTimeMillis - codeActiveTime)) 38 | .map(_.usedAt).update(Some(System.currentTimeMillis)) 39 | } 40 | } 41 | } 42 | 43 | def getIdentity(userIdentifier: String): Long = { 44 | blocking { 45 | db.withSession { implicit s => 46 | authEntriesQuery.filter(line => line.userIdentifier === userIdentifier).map(_.identityId).first 47 | } 48 | } 49 | } 50 | 51 | def getNextCardIndex(userIdentifier: String): Long = { 52 | blocking { 53 | db.withSession { implicit s => 54 | s.withTransaction { 55 | val next = authEntriesQuery.filter(line => line.userIdentifier === userIdentifier).map(_.lastCard).first + 1 56 | authEntriesQuery.filter(line => line.userIdentifier === userIdentifier).map(_.lastCard).update(next) 57 | next 58 | } 59 | } 60 | } 61 | } 62 | 63 | def saveAuthEntryAndCodeCard(authEntry: AuthEntry, codeCard: CodeCard): Unit = { 64 | blocking { 65 | db.withSession { implicit s => 66 | s.withTransaction { 67 | authEntriesQuery += authEntry 68 | codeCard.codes.zipWithIndex.map { case (code, idx) => 69 | codesQuery += Code(codeCard.userIdentifier, codeCard.id, idx.toLong, code, System.currentTimeMillis()) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | def getInactiveCodesForUser(userIdentifier: String): Seq[Code] = { 77 | blocking { 78 | db.withSession { implicit s => 79 | codesQuery.filter(code => code.userIdentifier === userIdentifier && code.activatedAt.isEmpty === true).list 80 | } 81 | } 82 | } 83 | 84 | def activateCode(userIdentifier: String, cardIndex: Long, codeIndex: Long): Int = { 85 | blocking { 86 | db.withSession { implicit s => 87 | codesQuery.filter(code => code.userIdentifier === userIdentifier && 88 | code.cardIndex === cardIndex && 89 | code.codeIndex === codeIndex).map(_.activatedAt).update(Some(System.currentTimeMillis)) 90 | } 91 | } 92 | } 93 | 94 | private val codesQuery = TableQuery[Codes] 95 | private val authEntriesQuery = TableQuery[AuthEntries] 96 | 97 | private val db = Database.forURL(url = dbUrl, user = dbUser, password = dbPassword, driver = "org.postgresql.Driver") 98 | } 99 | -------------------------------------------------------------------------------- /auth-codecard/src/main/scala/Service.scala: -------------------------------------------------------------------------------- 1 | import java.security.SecureRandom 2 | import scala.concurrent.{Future, ExecutionContext} 3 | 4 | class Service(gateway: Gateway, repository: Repository)(implicit ec: ExecutionContext) extends Config { 5 | def register(tokenValueOption: Option[String]): Future[Either[String, RegisterResponse]] = 6 | acquireIdentity(tokenValueOption).map { 7 | case Right(identity) => 8 | val authEntry = generateAuthEntry(identity) 9 | val codeCard = generateCodeCard(1, authEntry.userIdentifier) 10 | repository.saveAuthEntryAndCodeCard(authEntry, codeCard) 11 | Right(RegisterResponse(identity, codeCard)) 12 | case Left(l) => Left(l) 13 | } 14 | 15 | def activateCode(request: ActivateCodeRequest): Future[Either[String, ActivateCodeResponse]] = { 16 | Future.successful { 17 | val codes = repository.getInactiveCodesForUser(request.userIdentifier) 18 | codes.length match { 19 | case 0 => Left("You don't have available codes") 20 | case _ => 21 | val codeAct = codes(random.nextInt(codes.length)) 22 | repository.activateCode(request.userIdentifier, codeAct.cardIndex, codeAct.codeIndex) 23 | Right(ActivateCodeResponse(codeAct.cardIndex, codeAct.codeIndex)) 24 | } 25 | } 26 | } 27 | 28 | def login(request: LoginRequest, tokenValueOption: Option[String]): Future[Either[String, Token]] = { 29 | repository.useCode(request.userIdentifier, request.cardIndex, request.codeIndex, request.code) match { 30 | case 1 => 31 | tokenValueOption match { 32 | case None => gateway.requestLogin(repository.getIdentity(request.userIdentifier)).map(Right(_)) 33 | case Some(tokenValue) => 34 | gateway.requestRelogin(tokenValue).map { 35 | case Some(token) => Right(token) 36 | case None => Left("Token expired or not found") 37 | } 38 | } 39 | case 0 => Future.successful(Left(s"Invalid code")) 40 | } 41 | } 42 | 43 | def getCodeCard(request: GetCodeCardRequest, tokenValueOption: Option[String]): Future[Either[String, GetCodeCardResponse]] = { 44 | tokenValueOption match { 45 | case Some(tokenValue) => 46 | gateway.requestRelogin(tokenValue).map { 47 | case None => Left("Token expired or not found") 48 | case Some(token) if repository.getIdentity(request.userIdentifier) == token.identityId => 49 | Right(GetCodeCardResponse(request.userIdentifier, generateCodeCard(repository.getNextCardIndex(request.userIdentifier), request.userIdentifier))) 50 | case Some(token) => Left("Token expired or not found") 51 | } 52 | case None => Future.successful(Left("Token expired or not found")) 53 | } 54 | } 55 | 56 | private def acquireIdentity(tokenValueOption: Option[String]): Future[Either[String, Identity]] = { 57 | tokenValueOption match { 58 | case Some(tokenValue) => gateway.requestToken(tokenValue).map(_.right.map(token => Identity(token.identityId))) 59 | case None => gateway.requestNewIdentity().map(Right(_)) 60 | } 61 | } 62 | 63 | private def generateAuthEntry(identity: Identity) = { 64 | AuthEntry(f"${random.nextInt(100000)}%05d${random.nextInt(100000)}%05d", identity.id, System.currentTimeMillis(), 1) 65 | } 66 | 67 | private def generateCodeCard(cardIndex: Long, userIdentifier: String) = { 68 | CodeCard(cardIndex, Seq.fill(cardSize) {f"${random.nextInt(1000000)}%06d" }, userIdentifier) 69 | } 70 | 71 | private val random = new SecureRandom 72 | } -------------------------------------------------------------------------------- /auth-fb/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8001 8 | } 9 | 10 | redis { 11 | host = "localhost" 12 | port = 6379 13 | password = "" 14 | db = 0 15 | } 16 | 17 | services { 18 | identity-manager { 19 | host = "localhost" 20 | port = 8000 21 | } 22 | 23 | token-manager { 24 | host = "localhost" 25 | port = 8010 26 | } 27 | } -------------------------------------------------------------------------------- /auth-fb/src/main/scala/AuthFb.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 4 | import akka.http.scaladsl.marshalling.ToResponseMarshallable 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.stream.ActorFlowMaterializer 8 | import com.restfb.exception.FacebookException 9 | import scala.util.{Failure => FailureT, Success => SuccessT} 10 | 11 | case class AuthResponse(accessToken: String) 12 | 13 | case class Identity(id: Long) 14 | case class Token(value: String, validTo: Long, identityId: Long, authMethods: Set[String]) 15 | 16 | object AuthFb extends App with JsonProtocols with Config { 17 | implicit val actorSystem = ActorSystem() 18 | implicit val materializer = ActorFlowMaterializer() 19 | implicit val dispatcher = actorSystem.dispatcher 20 | 21 | val gateway = new Gateway 22 | val service = new Service(gateway) 23 | 24 | Http().bindAndHandle(interface = interface, port = port, handler = { 25 | logRequestResult("auth-fb") { 26 | (path("register" / "fb") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => 27 | complete { 28 | service.register(authResponse, tokenValue) match { 29 | case SuccessT(f) => f.map[ToResponseMarshallable] { 30 | case Right(identity) => Created -> identity 31 | case Left(errorMessage) => BadRequest -> errorMessage 32 | } 33 | case FailureT(e: FacebookException) => Unauthorized -> e.getMessage 34 | case _ => InternalServerError 35 | } 36 | } 37 | } ~ 38 | (path("login" / "fb") & pathEndOrSingleSlash & post & entity(as[AuthResponse]) & optionalHeaderValueByName("Auth-Token")) { (authResponse, tokenValue) => 39 | complete { 40 | service.login(authResponse, tokenValue) match { 41 | case SuccessT(f) => f.map[ToResponseMarshallable] { 42 | case Right(token) => Created -> token 43 | case Left(errorMessage) => BadRequest -> errorMessage 44 | } 45 | case FailureT(e: FacebookException) => Unauthorized -> e.getMessage 46 | case _ => InternalServerError 47 | } 48 | } 49 | } 50 | } 51 | }) 52 | } -------------------------------------------------------------------------------- /auth-fb/src/main/scala/Config.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.config.ConfigFactory 2 | 3 | trait Config { 4 | protected val config = ConfigFactory.load() 5 | protected val interface = config.getString("http.interface") 6 | protected val port = config.getInt("http.port") 7 | protected val redisHost = config.getString("redis.host") 8 | protected val redisPort = config.getInt("redis.port") 9 | protected val redisPassword = config.getString("redis.password") 10 | protected val redisDb = config.getInt("redis.db") 11 | 12 | protected val identityManagerHost = config.getString("services.identity-manager.host") 13 | protected val identityManagerPort = config.getInt("services.identity-manager.port") 14 | protected val tokenManagerHost = config.getString("services.token-manager.host") 15 | protected val tokenManagerPort = config.getInt("services.token-manager.port") 16 | } -------------------------------------------------------------------------------- /auth-fb/src/main/scala/Gateway.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.client.RequestBuilding 4 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 7 | import akka.http.scaladsl.unmarshalling.Unmarshal 8 | import akka.stream.FlowMaterializer 9 | import akka.stream.scaladsl.{Sink, Source} 10 | import com.restfb.DefaultFacebookClient 11 | import com.restfb.types.User 12 | import java.io.IOException 13 | import scala.concurrent.{blocking, ExecutionContext, Future} 14 | import scala.util.Try 15 | 16 | case class InternalLoginRequest(identityId: Long, authMethod: String = "fb") 17 | case class InternalReloginRequest(tokenValue: String, authMethod: String = "fb") 18 | 19 | class Gateway(implicit actorSystem: ActorSystem, materializer: FlowMaterializer, ec: ExecutionContext) 20 | extends JsonProtocols with Config { 21 | 22 | private val identityManagerConnectionFlow = Http().outgoingConnection(identityManagerHost, identityManagerPort) 23 | private val tokenManagerConnectionFlow = Http().outgoingConnection(tokenManagerHost, tokenManagerPort) 24 | 25 | private def requestIdentityManager(request: HttpRequest): Future[HttpResponse] = { 26 | Source.single(request).via(identityManagerConnectionFlow).runWith(Sink.head) 27 | } 28 | 29 | private def requestTokenManager(request: HttpRequest): Future[HttpResponse] = { 30 | Source.single(request).via(tokenManagerConnectionFlow).runWith(Sink.head) 31 | } 32 | 33 | def requestToken(tokenValue: String): Future[Either[String, Token]] = { 34 | requestTokenManager(RequestBuilding.Get(s"/tokens/$tokenValue")).flatMap { response => 35 | response.status match { 36 | case Success(_) => Unmarshal(response.entity).to[Token].map(Right(_)) 37 | case NotFound => Future.successful(Left("Token expired or not found")) 38 | case _ => Future.failed(new IOException(s"Token request failed with status ${response.status} and error ${response.entity}")) 39 | } 40 | } 41 | } 42 | 43 | def requestNewIdentity(): Future[Identity] = { 44 | requestIdentityManager(RequestBuilding.Post("/identities")).flatMap { response => 45 | response.status match { 46 | case Success(_) => Unmarshal(response.entity).to[Identity] 47 | case _ => Future.failed(new IOException(s"Identity request failed with status ${response.status} and error ${response.entity}")) 48 | } 49 | } 50 | } 51 | 52 | def requestLogin(identityId: Long): Future[Token] = { 53 | val loginRequest = InternalLoginRequest(identityId) 54 | requestTokenManager(RequestBuilding.Post("/tokens", loginRequest)).flatMap { response => 55 | response.status match { 56 | case Success(_) => Unmarshal(response.entity).to[Token] 57 | case _ => Future.failed(new IOException(s"Login request failed with status ${response.status} and error ${response.entity}")) 58 | } 59 | } 60 | } 61 | 62 | def requestRelogin(tokenValue: String): Future[Option[Token]] = { 63 | requestTokenManager(RequestBuilding.Patch("/tokens", InternalReloginRequest(tokenValue))).flatMap { response => 64 | response.status match { 65 | case Success(_) => Unmarshal(response.entity).to[Token].map(Option(_)) 66 | case NotFound => Future.successful(None) 67 | case _ => Future.failed(new IOException(s"Relogin request failed with status ${response.status} and error ${response.entity}")) 68 | } 69 | } 70 | } 71 | 72 | def getFbUserDetails(accessToken: String): Try[User] = { 73 | Try { 74 | blocking { 75 | val client = new DefaultFacebookClient(accessToken) 76 | client.fetchObject("me", classOf[User]) 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /auth-fb/src/main/scala/JsonProtocols.scala: -------------------------------------------------------------------------------- 1 | import spray.json.DefaultJsonProtocol 2 | 3 | trait JsonProtocols extends DefaultJsonProtocol { 4 | protected implicit val authResponseFormat = jsonFormat1(AuthResponse.apply) 5 | protected implicit val identityFormat = jsonFormat1(Identity.apply) 6 | protected implicit val loginRequestFormat = jsonFormat2(InternalLoginRequest.apply) 7 | protected implicit val reloginRequestFormat = jsonFormat2(InternalReloginRequest.apply) 8 | protected implicit val tokenFormat = jsonFormat4(Token.apply) 9 | } -------------------------------------------------------------------------------- /auth-fb/src/main/scala/Service.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import com.restfb.types.User 3 | import redis.RedisClient 4 | import scala.concurrent.{ExecutionContext, Future} 5 | import scala.util.Try 6 | 7 | class Service(gateway: Gateway)(implicit actorSystem: ActorSystem, ec: ExecutionContext) extends Config { 8 | def register(authResponse: AuthResponse, tokenValueOption: Option[String]): Try[Future[Either[String, Identity]]] = { 9 | gateway.getFbUserDetails(authResponse.accessToken).map { user => 10 | redis.exists(userToRedisKey(user)).flatMap { 11 | case true => Future.successful(Left(s"User with id ${user.getId} is already registered")) 12 | case false => acquireIdentity(tokenValueOption).flatMap { 13 | case Right(identity) => saveUserIdentityMapping(user, identity) 14 | case l => Future.successful(l) 15 | } 16 | } 17 | } 18 | } 19 | 20 | def login(authResponse: AuthResponse, tokenValueOption: Option[String]): Try[Future[Either[String, Token]]] = { 21 | gateway.getFbUserDetails(authResponse.accessToken).map { user => 22 | getIdentityIdForUser(user).flatMap { 23 | case Some(identityId) => doLogin(identityId, tokenValueOption) 24 | case None => Future.successful(Left(s"User with id ${user.getId} is not registered")) 25 | } 26 | } 27 | } 28 | 29 | private def doLogin(identityId: Long, tokenValueOption: Option[String]): Future[Either[String, Token]] = { 30 | tokenValueOption match { 31 | case Some(tokenValue) => gateway.requestRelogin(tokenValue).map { 32 | case Some(token) => Right(token) 33 | case None => Left("Token expired or not found") 34 | } 35 | case None => gateway.requestLogin(identityId).map(Right(_)) 36 | } 37 | } 38 | 39 | private def acquireIdentity(tokenValueOption: Option[String]): Future[Either[String, Identity]] = { 40 | tokenValueOption match { 41 | case Some(tokenValue) => gateway.requestToken(tokenValue).map(_.right.map(token => Identity(token.identityId))) 42 | case None => gateway.requestNewIdentity().map(Right(_)) 43 | } 44 | } 45 | 46 | private def saveUserIdentityMapping(user: User, identity: Identity): Future[Either[String, Identity]] = { 47 | redis.setnx(userToRedisKey(user), identity.id).map { 48 | case true => Right(identity) 49 | case false => Left(s"User with id ${user.getId} is already registered") 50 | } 51 | } 52 | 53 | private def getIdentityIdForUser(user: User): Future[Option[Long]] = redis.get(userToRedisKey(user)).map(_.map(_.utf8String.toLong)) 54 | 55 | private def userToRedisKey(user: User): String = s"auth-fb:id:${user.getId}" 56 | 57 | private val redis = RedisClient(host = redisHost, port = redisPort, password = Option(redisPassword), db = Option(redisDb)) 58 | } 59 | -------------------------------------------------------------------------------- /auth-password/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8002 8 | } 9 | 10 | db { 11 | url = "jdbc:postgresql://localhost:5432/auth_password" 12 | user = "postgres" 13 | password = "postgres" 14 | } 15 | 16 | services { 17 | identity-manager { 18 | host = "localhost" 19 | port = 8000 20 | } 21 | 22 | token-manager { 23 | host = "localhost" 24 | port = 8010 25 | } 26 | } -------------------------------------------------------------------------------- /auth-password/src/main/scala/AuthPassword.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 4 | import akka.http.scaladsl.marshalling.ToResponseMarshallable 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.stream.ActorFlowMaterializer 8 | 9 | case class PasswordRegisterRequest(email: EmailAddress, password: String) 10 | case class PasswordLoginRequest(email: EmailAddress, password: String) 11 | case class PasswordResetRequest(email: EmailAddress, newPassword: String) 12 | 13 | case class Identity(id: Long) 14 | case class Token(value: String, validTo: Long, identityId: Long, authMethods: Set[String]) 15 | 16 | object AuthPassword extends App with JsonProtocols with Config { 17 | implicit val actorSystem = ActorSystem() 18 | implicit val materializer = ActorFlowMaterializer() 19 | implicit val dispatcher = actorSystem.dispatcher 20 | 21 | val repository = new Repository 22 | val gateway = new Gateway 23 | val service = new Service(repository, gateway) 24 | 25 | Http().bindAndHandle(interface = interface, port = port, handler = { 26 | logRequestResult("auth-password") { 27 | path("register" / "password") { 28 | (pathEndOrSingleSlash & post & entity(as[PasswordRegisterRequest]) & optionalHeaderValueByName("Auth-Token")) { 29 | (request, tokenValue) => 30 | complete { 31 | service.register(request, tokenValue).map[ToResponseMarshallable] { 32 | case Right(identity) => Created -> identity 33 | case Left(errorMessage) => BadRequest -> errorMessage 34 | } 35 | } 36 | } 37 | } ~ 38 | path("login" / "password") { 39 | (pathEndOrSingleSlash & post & entity(as[PasswordLoginRequest]) & optionalHeaderValueByName("Auth-Token")) { 40 | (request, tokenValue) => 41 | complete { 42 | service.login(request, tokenValue).map[ToResponseMarshallable] { 43 | case Right(token) => Created -> token 44 | case Left(errorMessage) => BadRequest -> errorMessage 45 | } 46 | } 47 | } 48 | } ~ 49 | path("reset" / "password") { 50 | (pathEndOrSingleSlash & post & entity(as[PasswordResetRequest]) & headerValueByName("Auth-Token")) { 51 | (request, tokenValue) => 52 | complete { 53 | service.reset(request, tokenValue).map[ToResponseMarshallable] { 54 | case Right(identity) => OK -> identity 55 | case Left(errorMessage) => BadRequest -> errorMessage 56 | } 57 | } 58 | } 59 | } 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /auth-password/src/main/scala/Config.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.config.ConfigFactory 2 | 3 | trait Config { 4 | protected val config = ConfigFactory.load() 5 | protected val interface = config.getString("http.interface") 6 | protected val port = config.getInt("http.port") 7 | protected val dbUrl = config.getString("db.url") 8 | protected val dbUser = config.getString("db.user") 9 | protected val dbPassword = config.getString("db.password") 10 | 11 | protected val identityManagerHost = config.getString("services.identity-manager.host") 12 | protected val identityManagerPort = config.getInt("services.identity-manager.port") 13 | protected val tokenManagerHost = config.getString("services.token-manager.host") 14 | protected val tokenManagerPort = config.getInt("services.token-manager.port") 15 | } 16 | -------------------------------------------------------------------------------- /auth-password/src/main/scala/Gateway.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.client.RequestBuilding 4 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 7 | import akka.http.scaladsl.unmarshalling.Unmarshal 8 | import akka.stream.FlowMaterializer 9 | import akka.stream.scaladsl.{Sink, Source} 10 | import java.io.IOException 11 | import scala.concurrent.{ExecutionContext, Future} 12 | 13 | case class InternalLoginRequest(identityId: Long, authMethod: String = "password") 14 | case class InternalReloginRequest(tokenValue: String, authMethod: String = "password") 15 | 16 | class Gateway(implicit actorSystem: ActorSystem, materializer: FlowMaterializer, ec: ExecutionContext) 17 | extends JsonProtocols with Config { 18 | 19 | private val identityManagerConnectionFlow = Http().outgoingConnection(identityManagerHost, identityManagerPort) 20 | private val tokenManagerConnectionFlow = Http().outgoingConnection(tokenManagerHost, tokenManagerPort) 21 | 22 | private def requestIdentityManager(request: HttpRequest): Future[HttpResponse] = { 23 | Source.single(request).via(identityManagerConnectionFlow).runWith(Sink.head) 24 | } 25 | 26 | private def requestTokenManager(request: HttpRequest): Future[HttpResponse] = { 27 | Source.single(request).via(tokenManagerConnectionFlow).runWith(Sink.head) 28 | } 29 | 30 | def requestToken(tokenValue: String): Future[Either[String, Token]] = { 31 | requestTokenManager(RequestBuilding.Get(s"/tokens/$tokenValue")).flatMap { response => 32 | response.status match { 33 | case Success(_) => Unmarshal(response.entity).to[Token].map(Right(_)) 34 | case NotFound => Future.successful(Left("Token expired or not found")) 35 | case _ => Future.failed(new IOException(s"Token request failed with status ${response.status} and error ${response.entity}")) 36 | } 37 | } 38 | } 39 | 40 | def requestNewIdentity(): Future[Identity] = { 41 | requestIdentityManager(RequestBuilding.Post("/identities")).flatMap { response => 42 | response.status match { 43 | case Success(_) => Unmarshal(response.entity).to[Identity] 44 | case _ => Future.failed(new IOException(s"Identity request failed with status ${response.status} and error ${response.entity}")) 45 | } 46 | } 47 | } 48 | 49 | def requestLogin(identityId: Long): Future[Token] = { 50 | val loginRequest = InternalLoginRequest(identityId) 51 | requestTokenManager(RequestBuilding.Post("/tokens", loginRequest)).flatMap { response => 52 | response.status match { 53 | case Success(_) => Unmarshal(response.entity).to[Token] 54 | case _ => Future.failed(new IOException(s"Login request failed with status ${response.status} and error ${response.entity}")) 55 | } 56 | } 57 | } 58 | 59 | def requestRelogin(tokenValue: String): Future[Option[Token]] = { 60 | requestTokenManager(RequestBuilding.Patch("/tokens", InternalReloginRequest(tokenValue))).flatMap { response => 61 | response.status match { 62 | case Success(_) => Unmarshal(response.entity).to[Token].map(Option(_)) 63 | case NotFound => Future.successful(None) 64 | case _ => Future.failed(new IOException(s"Relogin request failed with status ${response.status} and error ${response.entity}")) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /auth-password/src/main/scala/JsonProtocols.scala: -------------------------------------------------------------------------------- 1 | import spray.json._ 2 | 3 | trait JsonProtocols extends DefaultJsonProtocol { 4 | protected implicit val emailFormat = new JsonFormat[EmailAddress] { 5 | override def write(obj: EmailAddress): JsValue = JsString(obj.address) 6 | 7 | override def read(json: JsValue): EmailAddress = json match { 8 | case JsString(value) => EmailAddress(value) 9 | case _ => deserializationError("Email address expected") 10 | } 11 | } 12 | 13 | protected implicit val passwordRegisterRequestFormat = jsonFormat2(PasswordRegisterRequest) 14 | protected implicit val passwordLoginRequestFormat = jsonFormat2(PasswordLoginRequest) 15 | protected implicit val resetRequestFormat = jsonFormat2(PasswordResetRequest) 16 | protected implicit val identityFormat = jsonFormat1(Identity) 17 | protected implicit val tokenFormat = jsonFormat4(Token) 18 | protected implicit val loginRequestFormat = jsonFormat2(InternalLoginRequest) 19 | protected implicit val reloginRequestFormat = jsonFormat2(InternalReloginRequest) 20 | } 21 | -------------------------------------------------------------------------------- /auth-password/src/main/scala/Repository.scala: -------------------------------------------------------------------------------- 1 | import scala.concurrent.blocking 2 | import scala.slick.driver.PostgresDriver.simple._ 3 | import scala.slick.jdbc.meta.MTable 4 | import scala.slick.lifted.{ProvenShape, Tag} 5 | 6 | case class EmailAddress(address: String) extends MappedTo[String] { 7 | override val value: String = address 8 | 9 | require(EmailAddress.isValid(address), "Invalid email address format") 10 | } 11 | 12 | object EmailAddress { 13 | def isValid(email: String): Boolean = EmailRegex.pattern.matcher(email.toUpperCase).matches() 14 | 15 | private val EmailRegex = """\b[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*\b""".r 16 | } 17 | 18 | case class AuthEntry(id: Option[Long], identityId: Long, createdAt: Long, email: EmailAddress, password: String) 19 | 20 | class AuthEntries(tag: Tag) extends Table[AuthEntry](tag, "auth_entry") { 21 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 22 | 23 | def identityId = column[Long]("identity_id", O.NotNull) 24 | 25 | def createdAt = column[Long]("created_at", O.NotNull) 26 | 27 | def email = column[EmailAddress]("email", O.NotNull) 28 | 29 | def password = column[String]("password", O.NotNull) 30 | 31 | override def * : ProvenShape[AuthEntry] = (id.?, identityId, createdAt, email, password) <> (AuthEntry.tupled, AuthEntry.unapply) 32 | } 33 | 34 | class Repository extends Config { 35 | def createAuthEntry(entry: AuthEntry) = { 36 | blocking { 37 | db.withSession { implicit session => 38 | authEntries.insert(entry).run 39 | } 40 | } 41 | } 42 | 43 | def updateAuthEntry(entry: AuthEntry) = { 44 | blocking { 45 | db.withSession { implicit session => 46 | authEntries.filter(_.id === entry.id.get).update(entry) 47 | } 48 | } 49 | } 50 | 51 | def findAuthEntry(email: EmailAddress): Option[AuthEntry] = { 52 | blocking { 53 | db.withSession { implicit session => 54 | byEmailCompiled(email).firstOption 55 | } 56 | } 57 | } 58 | 59 | private def byEmailQuery(email: Column[EmailAddress]) = authEntries.filter(_.email === email) 60 | private val byEmailCompiled = Compiled(byEmailQuery _) 61 | 62 | private val authEntries = TableQuery[AuthEntries] 63 | 64 | private val db = Database.forURL(url = dbUrl, user = dbUser, password = dbPassword, driver = "org.postgresql.Driver") 65 | } -------------------------------------------------------------------------------- /auth-password/src/main/scala/Service.scala: -------------------------------------------------------------------------------- 1 | import org.mindrot.jbcrypt.BCrypt 2 | import scala.concurrent.{ExecutionContext, Future} 3 | 4 | class Service(repository: Repository, gateway: Gateway)(implicit ec: ExecutionContext) { 5 | def register(request: PasswordRegisterRequest, tokenValueOption: Option[String]): Future[Either[String, Identity]] = { 6 | if (repository.findAuthEntry(request.email).isDefined) { 7 | Future.successful(Left(s"Wrong login data")) 8 | } else { 9 | acquireIdentity(tokenValueOption).map { 10 | case Right(identity) => Right(createEntry(request, identity)) 11 | case l => l 12 | } 13 | } 14 | } 15 | 16 | def login(request: PasswordLoginRequest, tokenValueOption: Option[String]): Future[Either[String, Token]] = { 17 | repository.findAuthEntry(request.email) match { 18 | case None => Future.successful(Left(s"Wrong login data")) 19 | case Some(entry) => 20 | if (!checkPassword(request.password, entry.password)) { 21 | Future.successful(Left(s"Wrong login data")) 22 | } else { 23 | tokenValueOption match { 24 | case None => gateway.requestLogin(entry.identityId).map(Right(_)) 25 | case Some(tokenValue) => 26 | gateway.requestRelogin(tokenValue).map { 27 | case Some(token) => Right(token) 28 | case None => Left("Token expired or not found") 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | def reset(request: PasswordResetRequest, tokenValue: String): Future[Either[String, Identity]] = { 36 | repository.findAuthEntry(request.email) match { 37 | case None => Future.successful(Left(s"Wrong login data")) 38 | case Some(entry) => 39 | gateway.requestToken(tokenValue).flatMap { 40 | case Right(token) => 41 | if (entry.identityId != token.identityId) { 42 | Future.successful(Left(s"Wrong login data")) 43 | } 44 | else { 45 | val passHash = hashPassword(request.newPassword) 46 | repository.updateAuthEntry(entry.copy(password = passHash)) 47 | Future.successful(Right(Identity(entry.identityId))) 48 | } 49 | case Left(s) => Future.successful(Left(s)) 50 | } 51 | } 52 | } 53 | 54 | private def createEntry(request: PasswordRegisterRequest, identity: Identity): Identity = { 55 | val passHash = hashPassword(request.password) 56 | val entry = AuthEntry(None, identity.id, System.currentTimeMillis, request.email, passHash) 57 | repository.createAuthEntry(entry) 58 | identity 59 | } 60 | 61 | private def acquireIdentity(tokenValueOption: Option[String]): Future[Either[String, Identity]] = { 62 | tokenValueOption match { 63 | case Some(tokenValue) => gateway.requestToken(tokenValue).map(_.right.map(token => Identity(token.identityId))) 64 | case None => gateway.requestNewIdentity().map(Right(_)) 65 | } 66 | } 67 | 68 | private def hashPassword(password: String): String = BCrypt.hashpw(password, BCrypt.gensalt(12)) 69 | 70 | private def checkPassword(password: String, passwordHash: String): Boolean = BCrypt.checkpw(password, passwordHash) 71 | } 72 | -------------------------------------------------------------------------------- /btc-common/src/main/scala/btc/common/Messages.scala: -------------------------------------------------------------------------------- 1 | package btc.common 2 | 3 | import akka.actor.ActorRef 4 | 5 | object UserManagerMessages { 6 | case class LookupUser(id: Long) 7 | } 8 | 9 | object UserHandlerMessages { 10 | sealed trait Command { 11 | val id: Long 12 | } 13 | 14 | sealed trait Subscribe extends Command 15 | 16 | sealed trait ThresholdSubscribe extends Subscribe { 17 | val threshold: BigDecimal 18 | } 19 | 20 | case class SubscribeRateChange(override val id: Long) extends Subscribe 21 | case class SubscribeBidOver(override val id: Long, override val threshold: BigDecimal) extends ThresholdSubscribe 22 | case class SubscribeAskBelow(override val id: Long, override val threshold: BigDecimal) extends ThresholdSubscribe 23 | case class SubscribeVolumeOver(override val id: Long, override val threshold: BigDecimal) extends ThresholdSubscribe 24 | case class SubscribeVolumeBelow(override val id: Long, override val threshold: BigDecimal) extends ThresholdSubscribe 25 | 26 | case class Unsubscribe(override val id: Long) extends Command 27 | 28 | case object Heartbeat 29 | 30 | case object QuerySubscriptions 31 | } 32 | 33 | object WebSocketHandlerMessages { 34 | sealed trait MarketEvent 35 | 36 | case class OperationSuccessful(id: Long) extends MarketEvent 37 | case class Alarm(id: Long, value: BigDecimal) extends MarketEvent 38 | case class AllSubscriptions(subscriptions: Seq[UserHandlerMessages.Subscribe]) extends MarketEvent 39 | 40 | case class InitActorResponse(actor: ActorRef) 41 | } 42 | -------------------------------------------------------------------------------- /btc-users/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | application { 2 | name = "btc-users" 3 | } 4 | 5 | akka { 6 | remote.netty.tcp { 7 | hostname = "127.0.0.1" 8 | port = 2551 9 | } 10 | actor.provider = "akka.remote.RemoteActorRefProvider" 11 | loglevel = "INFO" 12 | } 13 | 14 | data-fetcher.interval = 1000 15 | 16 | user-handler.timeout = 10000 -------------------------------------------------------------------------------- /btc-users/src/main/scala/DataFetcher.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.{ActorLogging, Props, Actor, ActorRef} 2 | import com.ning.http.client.AsyncHttpClientConfig.Builder 3 | import play.api.libs.json.Json 4 | import play.api.libs.ws.ning.NingWSClient 5 | import UserHandler.Ticker 6 | 7 | class DataFetcher(broadcaster: ActorRef) extends Actor with ActorLogging { 8 | override def receive: Receive = { 9 | case DataFetcher.Tick => 10 | client.url(url).get().map { response => 11 | if (response.status == 200) { 12 | val ticker = Json.parse(response.body).as[Ticker] 13 | log.debug(s"Broadcasting ticker $ticker") 14 | broadcaster ! ticker 15 | } 16 | }.onFailure { case t => log.warning(s"Requesting ticker failed because ${t.getMessage}") } 17 | } 18 | 19 | private implicit val tickerFormat = Json.format[Ticker] 20 | private implicit val dispatcher = context.dispatcher 21 | private val url = "https://bitbay.net/API/Public/BTCUSD/ticker.json" 22 | private val client = new NingWSClient(new Builder().build()) 23 | } 24 | 25 | object DataFetcher { 26 | case object Tick 27 | 28 | def props(broadcaster: ActorRef): Props = Props(new DataFetcher(broadcaster)) 29 | } 30 | -------------------------------------------------------------------------------- /btc-users/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | import akka.actor._ 2 | import akka.routing.{NoRoutee, AddRoutee, BroadcastGroup} 3 | import com.typesafe.config.ConfigFactory 4 | import scala.concurrent.duration._ 5 | 6 | object Main extends App { 7 | val config = ConfigFactory.load() 8 | 9 | val applicationName = config.getString("application.name") 10 | val dataFetcherInterval = config.getLong("data-fetcher.interval").millis 11 | val keepAliveTimeout = config.getLong("user-handler.timeout").millis 12 | 13 | implicit val system = ActorSystem(applicationName, config) 14 | 15 | val broadcaster = system.actorOf(BroadcastGroup(List()).props()) 16 | broadcaster ! AddRoutee(NoRoutee) 17 | 18 | val dataFetcher = system.actorOf(DataFetcher.props(broadcaster)) 19 | system.scheduler.schedule(dataFetcherInterval, dataFetcherInterval, dataFetcher, DataFetcher.Tick)(system.dispatcher) 20 | 21 | val manager = system.actorOf(UsersManager.props(broadcaster, keepAliveTimeout), "users-manager") 22 | } 23 | -------------------------------------------------------------------------------- /btc-users/src/main/scala/UserHandler.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.{ActorLogging, ActorRef, PoisonPill, Props} 2 | import akka.persistence.PersistentActor 3 | import akka.routing.{RemoveRoutee, ActorRefRoutee, AddRoutee} 4 | import btc.common.UserHandlerMessages._ 5 | import btc.common.WebSocketHandlerMessages.{OperationSuccessful, Alarm, AllSubscriptions} 6 | import scala.collection.mutable 7 | import scala.concurrent.duration._ 8 | import UserHandler._ 9 | 10 | object UserHandler { 11 | case object KeepAlive 12 | 13 | case class Ticker(max: BigDecimal, min: BigDecimal, last: BigDecimal, bid: BigDecimal, ask: BigDecimal, vwap: BigDecimal, average: BigDecimal, volume: BigDecimal) 14 | 15 | def props(userId: Long, wsActor: ActorRef, broadcaster: ActorRef, keepAliveTimeout: FiniteDuration) = { 16 | Props(new UserHandler(userId, wsActor, broadcaster, keepAliveTimeout)) 17 | } 18 | } 19 | 20 | class UserHandler(userId: Long, wsActor: ActorRef, broadcaster: ActorRef, keepAliveTimeout: FiniteDuration) extends PersistentActor with ActorLogging { 21 | override val persistenceId: String = userId.toString 22 | 23 | override def preStart(): Unit = { 24 | super.preStart() 25 | broadcaster ! AddRoutee(ActorRefRoutee(self)) 26 | } 27 | 28 | override def postStop(): Unit = { 29 | super.postStop() 30 | broadcaster ! RemoveRoutee(ActorRefRoutee(self)) 31 | } 32 | 33 | override def receiveRecover: Receive = { 34 | case subscribe: Subscribe => updateState(subscribe) 35 | case unsubscribe: Unsubscribe => updateState(unsubscribe) 36 | } 37 | 38 | override def receiveCommand: Receive = { 39 | case KeepAlive if System.currentTimeMillis() - lastHeartBeatTime > keepAliveTimeout.toMillis => 40 | log.info(s"Timeout while waiting for heartbeat for user $userId, stopping") 41 | self ! PoisonPill 42 | case Heartbeat => 43 | log.debug(s"Got heartbeat for user $userId") 44 | lastHeartBeatTime = System.currentTimeMillis() 45 | sender() ! Heartbeat 46 | case QuerySubscriptions => 47 | log.info(s"Got request for subscriptions for user $userId") 48 | wsActor ! AllSubscriptions(subscriptions.values.toList) 49 | case ticker: Ticker => 50 | val alarms = getAlarmsForTicker(ticker) 51 | log.debug(s"Got ticker and sending alarms $alarms for user $userId") 52 | alarms.foreach(wsActor ! _) 53 | case subscribe: Subscribe => 54 | log.debug(s"Got subscribe request $subscribe for user $userId") 55 | persist(subscribe) { e => 56 | updateState(e) 57 | wsActor ! OperationSuccessful(e.id) 58 | } 59 | case unsubscribe: Unsubscribe => 60 | log.debug(s"Got unsubscribe request $unsubscribe for user $userId") 61 | persist(unsubscribe) { e => 62 | updateState(e) 63 | wsActor ! OperationSuccessful(e.id) 64 | } 65 | } 66 | 67 | private def updateState(subscribe: Subscribe) = subscriptions.put(subscribe.id, subscribe) 68 | 69 | private def updateState(unsubscribe: Unsubscribe) = subscriptions.remove(unsubscribe.id) 70 | 71 | private def getAlarmsForTicker(ticker: Ticker): List[Alarm] = { 72 | subscriptions.values.map { 73 | case SubscribeRateChange(id) => Option(Alarm(id, ticker.average)) 74 | case SubscribeBidOver(id, threshold) => if (ticker.bid > threshold) Option(Alarm(id, ticker.bid)) else None 75 | case SubscribeAskBelow(id, threshold) => if (ticker.ask < threshold) Option(Alarm(id, ticker.ask)) else None 76 | case SubscribeVolumeOver(id, threshold) => if (ticker.volume > threshold) Option(Alarm(id, ticker.volume)) else None 77 | case SubscribeVolumeBelow(id, threshold) => if (ticker.volume < threshold) Option(Alarm(id, ticker.volume)) else None 78 | }.toList.flatten 79 | } 80 | 81 | private val subscriptions = mutable.Map.empty[Long, Subscribe] 82 | private var lastHeartBeatTime = System.currentTimeMillis() 83 | } -------------------------------------------------------------------------------- /btc-users/src/main/scala/UsersManager.scala: -------------------------------------------------------------------------------- 1 | import akka.actor._ 2 | import btc.common.UserManagerMessages.LookupUser 3 | import btc.common.WebSocketHandlerMessages.InitActorResponse 4 | import scala.concurrent.duration.FiniteDuration 5 | 6 | object UsersManager { 7 | def props(broadcaster: ActorRef, keepAliveTimeout: FiniteDuration) = Props(new UsersManager(broadcaster, keepAliveTimeout)) 8 | } 9 | 10 | class UsersManager(broadcaster: ActorRef, keepAliveTimeout: FiniteDuration) extends Actor with ActorLogging { 11 | override def receive: Receive = { 12 | case LookupUser(id) => 13 | log.info(s"Got user lookup request with id $id") 14 | val userHandler = context.actorOf(UserHandler.props(id, sender(), broadcaster, keepAliveTimeout)) 15 | context.system.scheduler.schedule(keepAliveTimeout * 3, keepAliveTimeout, userHandler, UserHandler.KeepAlive)(context.system.dispatcher) 16 | sender() ! InitActorResponse(userHandler) 17 | } 18 | } -------------------------------------------------------------------------------- /btc-ws/app/controllers/BtcWs.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import akka.actor._ 4 | import btc.common.UserManagerMessages.LookupUser 5 | import btc.common.UserHandlerMessages._ 6 | import btc.common.WebSocketHandlerMessages._ 7 | import play.api.Play.current 8 | import play.api.libs.concurrent.Akka 9 | import play.api.libs.json._ 10 | import play.api.libs.ws.WS 11 | import play.api.mvc.WebSocket.FrameFormatter 12 | import play.api.mvc._ 13 | import play.api.libs.concurrent.Execution.Implicits._ 14 | import scala.concurrent.duration._ 15 | import WebSocketHandler._ 16 | 17 | trait Formats { 18 | implicit val userEventFormat = new Format[Command] { 19 | override def writes(o: Command): JsValue = o match { 20 | case SubscribeRateChange(id) => Json.obj("operation" -> "SubscribeRateChange", "id" -> id) 21 | case SubscribeBidOver(id, threshold) => Json.obj("operation" -> "SubscribeBidOver", "id" -> id, "threshold" -> threshold) 22 | case SubscribeAskBelow(id, threshold) => Json.obj("operation" -> "SubscribeAskBelow", "id" -> id, "threshold" -> threshold) 23 | case SubscribeVolumeOver(id, threshold) => Json.obj("operation" -> "SubscribeVolumeOver", "id" -> id, "threshold" -> threshold) 24 | case SubscribeVolumeBelow(id, threshold) => Json.obj("operation" -> "SubscribeVolumeBelow", "id" -> id, "threshold" -> threshold) 25 | case Unsubscribe(id) => Json.obj("operation" -> "Unsubscribe", "id" -> id) 26 | } 27 | 28 | override def reads(json: JsValue): JsResult[Command] = { 29 | val idOption = (json \ "id").asOpt[Int] 30 | val operationOption = (json \ "operation").asOpt[String] 31 | val thresholdOption = (json \ "threshold").asOpt[BigDecimal] 32 | (idOption, operationOption, thresholdOption) match { 33 | case (Some(id), Some(operation), Some(threshold)) => 34 | operation match { 35 | case "SubscribeBidOver" => JsSuccess(SubscribeBidOver(id, threshold)) 36 | case "SubscribeAskBelow" => JsSuccess(SubscribeAskBelow(id, threshold)) 37 | case "SubscribeVolumeOver" => JsSuccess(SubscribeVolumeOver(id, threshold)) 38 | case "SubscribeVolumeBelow" => JsSuccess(SubscribeVolumeBelow(id, threshold)) 39 | case _ => JsError("Unknown operation") 40 | } 41 | case (Some(id), Some(operation), None) => 42 | operation match { 43 | case "SubscribeRateChange" => JsSuccess(SubscribeRateChange(id)) 44 | case "Unsubscribe" => JsSuccess(Unsubscribe(id)) 45 | case _ => JsError("Unknown operation or missing 'threshold' field") 46 | } 47 | case _ => JsError("Missing fields 'id' and/or 'operation'") 48 | } 49 | } 50 | } 51 | 52 | implicit val commandFrameFormatter = FrameFormatter.jsonFrame[Command] 53 | 54 | implicit val marketEventFormat = new Format[MarketEvent] { 55 | override def writes(o: MarketEvent): JsValue = o match { 56 | case o: OperationSuccessful => Json.obj("operation" -> "OperationSuccessful", "id" -> o.id) 57 | case o: Alarm => Json.obj("operation" -> "Alarm", "id" -> o.id, "value" -> o.value) 58 | case o: AllSubscriptions => Json.obj("operation" -> "AllSubscriptions", "subscriptions" -> o.subscriptions) 59 | } 60 | 61 | override def reads(json: JsValue): JsResult[MarketEvent] = ??? // not needed because it's out type 62 | } 63 | 64 | implicit val marketEventFrameFormatter = FrameFormatter.jsonFrame[MarketEvent] 65 | 66 | implicit val tokenReadsFormat = Json.reads[Token] 67 | } 68 | 69 | case class Token(value: String, validTo: Long, identityId: Long, authMethods: Set[String]) 70 | 71 | object BtcWs extends Controller with Formats { 72 | def index(authToken: String) = WebSocket.tryAcceptWithActor[Command, MarketEvent] { implicit request => 73 | tokenManagerUrl(authToken).get().map { response => 74 | if (response.status == OK) { 75 | val token = Json.parse(response.body).as[Token] 76 | val usersManager = Akka.system.actorSelection(current.configuration.getString("services.btc-users.users-manager-path").get) 77 | Right(WebSocketHandler.props(token, usersManager, webSocketHandlerTimeout, _: ActorRef)) 78 | } else { 79 | Left(Unauthorized("Token expired or not found")) 80 | } 81 | } 82 | } 83 | 84 | private def tokenManagerUrl(authToken: String) = WS.url(s"http://$tokenManagerHost:$tokenManagerPort/tokens/$authToken") 85 | private val tokenManagerHost = current.configuration.getString("services.token-manager.host").get 86 | private val tokenManagerPort = current.configuration.getInt("services.token-manager.port").get 87 | private val webSocketHandlerTimeout = current.configuration.getLong("web-socket-handler.timeout").get.millis 88 | } 89 | 90 | object WebSocketHandler { 91 | case object Timeout 92 | case object KeepAlive 93 | 94 | def props(token: Token, usersManager: ActorSelection, keepAliveTimeout: FiniteDuration, out: ActorRef) = { 95 | Props(new WebSocketHandler(token, usersManager, keepAliveTimeout, out)) 96 | } 97 | } 98 | 99 | class WebSocketHandler(token: Token, usersManager: ActorSelection, keepAliveTimeout: FiniteDuration, out: ActorRef) extends Actor with ActorLogging { 100 | override def preStart(): Unit = requestHandlerWithTimeout() 101 | 102 | override def receive: Receive = waitForHandler 103 | 104 | private def waitForHandler: Receive = { 105 | case InitActorResponse(handler: ActorRef) => 106 | log.info(s"Got handler for user ${token.identityId}") 107 | handler ! QuerySubscriptions 108 | context.become(waitForSubscriptions(handler)) 109 | case Timeout => 110 | log.warning(s"Timeout while waiting for handler for user ${token.identityId}, closing connection") 111 | self ! PoisonPill 112 | } 113 | 114 | private def waitForSubscriptions(handler: ActorRef): Receive = { 115 | case subs @ AllSubscriptions(subscriptions) => 116 | log.info(s"Got subscriptions $subscriptions for user ${token.identityId}") 117 | out ! subs 118 | scheduleHeartbeatAndKeepAlive(handler) 119 | context.become(handleUser(handler, subscriptions)) 120 | case Timeout => 121 | log.warning(s"Timeout while waiting for subscriptions for user ${token.identityId}, closing connection") 122 | self ! PoisonPill 123 | } 124 | 125 | private def handleUser(handler: ActorRef, subscriptions: Seq[Subscribe]): Receive = { 126 | case command: Command => 127 | log.debug(s"Got command $command from user ${token.identityId}") 128 | handler ! command 129 | case event: MarketEvent => 130 | log.debug(s"Got market event $event for user ${token.identityId}") 131 | out ! event 132 | case Heartbeat => 133 | log.debug(s"Got heartbeat for user ${token.identityId}") 134 | lastHeartBeat = System.currentTimeMillis() 135 | scheduleHeartbeatAndKeepAlive(handler) 136 | case KeepAlive if System.currentTimeMillis() - lastHeartBeat > keepAliveTimeout.toMillis => 137 | log.warning(s"Timeout while handling user ${token.identityId}, restarting") 138 | requestHandlerWithTimeout() 139 | context.become(waitForHandler, discardOld = true) 140 | } 141 | 142 | private def requestHandlerWithTimeout(): Unit = { 143 | log.info(s"Requesting handler for user ${token.identityId}") 144 | usersManager ! LookupUser(token.identityId) 145 | context.system.scheduler.scheduleOnce(keepAliveTimeout, self, Timeout) 146 | } 147 | 148 | private def scheduleHeartbeatAndKeepAlive(handler: ActorRef): Unit = { 149 | context.system.scheduler.scheduleOnce(keepAliveTimeout / 3, handler, Heartbeat) 150 | context.system.scheduler.scheduleOnce(keepAliveTimeout, self, KeepAlive) 151 | } 152 | 153 | private var lastHeartBeat = System.currentTimeMillis() 154 | } 155 | -------------------------------------------------------------------------------- /btc-ws/conf/application.conf: -------------------------------------------------------------------------------- 1 | logger { 2 | root = DEBUG 3 | play = DEBUG 4 | application = DEBUG 5 | } 6 | 7 | services { 8 | token-manager { 9 | host = localhost 10 | port = 8010 11 | } 12 | 13 | btc-users { 14 | users-manager-path = "akka.tcp://btc-users@127.0.0.1:2551/user/users-manager" 15 | } 16 | } 17 | 18 | akka { 19 | actor.provider = "akka.remote.RemoteActorRefProvider" 20 | loglevel = "INFO" 21 | } 22 | 23 | web-socket-handler.timeout = 10000 -------------------------------------------------------------------------------- /btc-ws/conf/routes: -------------------------------------------------------------------------------- 1 | GET /btc/:authToken controllers.BtcWs.index(authToken: String) -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import play.PlayScala 2 | import sbt.Keys._ 3 | 4 | name := "reactive-microservices" 5 | 6 | organization := "com.theiterators" 7 | 8 | version := "1.0" 9 | 10 | lazy val `reactive-microservices` = (project in file(".")).aggregate(metricsCommon, `metrics-collector`, `token-manager`, 11 | `session-manager`, `identity-manager`, `auth-fb`, `auth-codecard`, `auth-password`, btcCommon, `btc-ws`, `btc-users`) 12 | 13 | lazy val metricsCommon = (project in file("metrics-common")).settings(commonSettings: _*) 14 | .settings( 15 | libraryDependencies ++= Seq( 16 | `akka-actor`, 17 | `akka-stream`, 18 | `akka-http-core`, 19 | `akka-http-scala`, 20 | `akka-http-spray` 21 | ), 22 | Revolver.settings 23 | ) 24 | 25 | lazy val `metrics-collector` = (project in file("metrics-collector")).dependsOn(metricsCommon).enablePlugins(PlayScala) 26 | .settings(commonSettings: _*) 27 | .settings( 28 | libraryDependencies ++= Seq( 29 | `akka-actor`, 30 | `akka-stream`, 31 | `akka-http-core`, 32 | `akka-http-scala`, 33 | `akka-http-spray`, 34 | reactivemongo 35 | ), 36 | Revolver.settings 37 | ) 38 | 39 | lazy val `token-manager` = (project in file("token-manager")).dependsOn(metricsCommon).settings(commonSettings: _*) 40 | .settings( 41 | libraryDependencies ++= Seq( 42 | `akka-actor`, 43 | `akka-stream`, 44 | `akka-http-core`, 45 | `akka-http-scala`, 46 | `akka-http-spray`, 47 | reactivemongo 48 | ), 49 | Revolver.settings 50 | ) 51 | 52 | lazy val `session-manager` = (project in file("session-manager")).settings(commonSettings: _*) 53 | .settings( 54 | libraryDependencies ++= Seq( 55 | `akka-actor`, 56 | `akka-stream`, 57 | `akka-http-core`, 58 | `akka-http-scala`, 59 | `akka-http-spray` 60 | ), 61 | Revolver.settings 62 | ) 63 | 64 | lazy val `identity-manager` = (project in file("identity-manager")).settings(commonSettings: _*) 65 | .settings( 66 | libraryDependencies ++= Seq( 67 | `akka-actor`, 68 | `akka-stream`, 69 | `akka-http-core`, 70 | `akka-http-scala`, 71 | `akka-http-spray`, 72 | slick, 73 | postgresql 74 | ), 75 | Revolver.settings 76 | ) 77 | 78 | lazy val `auth-fb` = (project in file("auth-fb")).settings(commonSettings: _*) 79 | .settings( 80 | libraryDependencies ++= Seq( 81 | `akka-actor`, 82 | `akka-stream`, 83 | `akka-http-core`, 84 | `akka-http-scala`, 85 | `akka-http-spray`, 86 | rediscala, 87 | restfb 88 | ), 89 | Revolver.settings 90 | ) 91 | 92 | lazy val `auth-codecard` = (project in file("auth-codecard")).settings(commonSettings: _*).settings(commonSettings: _*) 93 | .settings( 94 | libraryDependencies ++= Seq( 95 | `akka-actor`, 96 | `akka-stream`, 97 | `akka-http-core`, 98 | `akka-http-scala`, 99 | `akka-http-spray`, 100 | slick, 101 | postgresql 102 | ), 103 | Revolver.settings 104 | ) 105 | 106 | lazy val `auth-password` = (project in file("auth-password")).settings(commonSettings: _*) 107 | .settings( 108 | libraryDependencies ++= Seq( 109 | `akka-actor`, 110 | `akka-stream`, 111 | `akka-http-core`, 112 | `akka-http-scala`, 113 | `akka-http-spray`, 114 | slick, 115 | postgresql, 116 | jbcrypt 117 | ), 118 | Revolver.settings 119 | ) 120 | 121 | lazy val btcCommon = (project in file("btc-common")).settings(commonSettings: _*).settings(libraryDependencies ++= 122 | Seq( 123 | `akka-actor` 124 | ) 125 | ) 126 | 127 | lazy val `btc-ws` = (project in file("btc-ws")).dependsOn(btcCommon).enablePlugins(PlayScala).settings(commonSettings: _*) 128 | .settings(libraryDependencies ++= Seq( 129 | ws, 130 | `akka-actor`, 131 | `akka-remote` 132 | ) 133 | ) 134 | 135 | lazy val `btc-users` = (project in file("btc-users")).dependsOn(btcCommon).settings(commonSettings: _*) 136 | .settings(libraryDependencies ++= Seq( 137 | `akka-contrib`, 138 | `akka-actor`, 139 | `akka-persistence`, 140 | `play-ws`, 141 | `play-json` 142 | ) 143 | ) 144 | 145 | lazy val commonSettings = Seq( 146 | scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8"), 147 | scalaVersion := "2.11.7", 148 | resolvers ++= Seq("rediscala" at "http://dl.bintray.com/etaty/maven", 149 | "Typesafe Releases" at "http://repo.typesafe.com/typesafe/releases/") 150 | ) 151 | 152 | val akkaV = "2.3.10" 153 | val playV = "2.3.8" 154 | val akkaStreamV = "1.0-RC2" 155 | val reactiveMongoV = "0.10.5.0.akka23" 156 | val slickV = "2.1.0" 157 | val postgresV = "9.3-1102-jdbc41" 158 | val rediscalaV = "1.4.0" 159 | val restFbV = "1.7.0" 160 | val jbcryptV = "0.3m" 161 | 162 | val `akka-actor` = "com.typesafe.akka" %% "akka-actor" % akkaV 163 | val `akka-stream` = "com.typesafe.akka" %% "akka-stream-experimental" % akkaStreamV 164 | val `akka-http-core` = "com.typesafe.akka" %% "akka-http-core-experimental" % akkaStreamV 165 | val `akka-http-scala` = "com.typesafe.akka" %% "akka-http-scala-experimental" % akkaStreamV 166 | val `akka-http-spray` = "com.typesafe.akka" %% "akka-http-spray-json-experimental" % akkaStreamV 167 | val `akka-remote` = "com.typesafe.akka" %% "akka-remote" % akkaV 168 | val `akka-contrib` = "com.typesafe.akka" %% "akka-contrib" % akkaV 169 | val `akka-persistence` = "com.typesafe.akka" %% "akka-persistence-experimental" % akkaV 170 | val reactivemongo = "org.reactivemongo" %% "reactivemongo" % reactiveMongoV 171 | val slick = "com.typesafe.slick" %% "slick" % slickV 172 | val postgresql = "org.postgresql" % "postgresql" % postgresV 173 | val `play-ws` = "com.typesafe.play" %% "play-ws" % playV 174 | val `play-json` = "com.typesafe.play" %% "play-json" % playV 175 | val jbcrypt = "org.mindrot" % "jbcrypt" % jbcryptV 176 | val rediscala = "com.etaty.rediscala" %% "rediscala" % rediscalaV 177 | val restfb = "com.restfb" % "restfb" % restFbV 178 | 179 | val runAll = inputKey[Unit]("Runs all subprojects") 180 | 181 | runAll := { 182 | (run in Compile in `token-manager`).evaluated 183 | (run in Compile in `session-manager`).evaluated 184 | (run in Compile in `identity-manager`).evaluated 185 | (run in Compile in `auth-fb`).evaluated 186 | (run in Compile in `auth-codecard`).evaluated 187 | (run in Compile in `auth-password`).evaluated 188 | (run in Compile in `btc-users`).evaluated 189 | } 190 | 191 | fork in run := true 192 | 193 | // enables unlimited amount of resources to be used :-o just for runAll convenience 194 | concurrentRestrictions in Global := Seq( 195 | Tags.customLimit(_ => true) 196 | ) 197 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | image: redis 3 | ports: 4 | - "6379:6379" 5 | 6 | mongo: 7 | image: mongo 8 | ports: 9 | - "27017:27017" 10 | 11 | postgres: 12 | build: postgres 13 | ports: 14 | - "5432:5432" 15 | -------------------------------------------------------------------------------- /identity-manager/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8000 8 | } 9 | 10 | db { 11 | url = "jdbc:postgresql://localhost/identity_manager" 12 | user = "postgres" 13 | password = "" 14 | } 15 | -------------------------------------------------------------------------------- /identity-manager/src/main/scala/IdentityManager.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 4 | import akka.http.scaladsl.model.StatusCodes._ 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.stream.ActorFlowMaterializer 7 | import com.typesafe.config.ConfigFactory 8 | import scala.concurrent.blocking 9 | import scala.slick.driver.PostgresDriver.simple._ 10 | import scala.slick.lifted.{ProvenShape, Tag} 11 | import spray.json.DefaultJsonProtocol 12 | 13 | case class Identity(id: Option[Long], createdAt: Long) 14 | 15 | class Identities(tag: Tag) extends Table[Identity](tag, "identity") { 16 | def id = column[Long]("id", O.PrimaryKey, O.AutoInc) 17 | 18 | def createdAt = column[Long]("created_at", O.NotNull) 19 | 20 | override def * : ProvenShape[Identity] = (id.?, createdAt) <> ((Identity.apply _).tupled, Identity.unapply) 21 | } 22 | 23 | object IdentityManager extends App with DefaultJsonProtocol { 24 | val config = ConfigFactory.load() 25 | val interface = config.getString("http.interface") 26 | val port = config.getInt("http.port") 27 | val dbUrl = config.getString("db.url") 28 | val dbUser = config.getString("db.user") 29 | val dbPassword = config.getString("db.password") 30 | 31 | implicit val actorSystem = ActorSystem() 32 | implicit val materializer = ActorFlowMaterializer() 33 | implicit val dispatcher = actorSystem.dispatcher 34 | 35 | implicit val identityFormat = jsonFormat2(Identity.apply) 36 | 37 | val db = Database.forURL(url = dbUrl, user = dbUser, password = dbPassword, driver = "org.postgresql.Driver") 38 | val identities = TableQuery[Identities] 39 | 40 | def getAllIdentities(): List[Identity] = { 41 | blocking { 42 | db.withSession { implicit s => 43 | identities.list 44 | } 45 | } 46 | } 47 | 48 | def saveIdentity(identity: Identity): Identity = { 49 | blocking { 50 | db.withSession { implicit s => 51 | identities returning identities.map(_.id) into ((_, id) => identity.copy(id = Option(id))) += identity 52 | } 53 | } 54 | } 55 | 56 | Http().bindAndHandle(interface = interface, port = port, handler = { 57 | logRequestResult("identity-manager") { 58 | path("identities") { 59 | pathEndOrSingleSlash { 60 | post { 61 | complete { 62 | val newIdentity = Identity(id = None, createdAt = System.currentTimeMillis()) 63 | Created -> saveIdentity(newIdentity) 64 | } 65 | } ~ 66 | get { 67 | complete { 68 | getAllIdentities() 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | } 76 | -------------------------------------------------------------------------------- /metrics-collector/app/controllers/MetricsCollector.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import akka.actor.{Props, Actor, ActorRef} 4 | import akka.routing.{RemoveRoutee, ActorRefRoutee, AddRoutee} 5 | import global.FlowInitializer 6 | import metrics.common.{Counter, Value, Metric} 7 | import play.api.Play.current 8 | import play.api.libs.json._ 9 | import play.api.mvc.WebSocket.FrameFormatter 10 | import play.api.mvc._ 11 | 12 | object MetricsCollector extends Controller { 13 | implicit val valueJsonFormat = Json.format[Value] 14 | implicit val counterJsonFormat = Json.format[Counter] 15 | implicit val metricJsonFormat = new Format[Metric] { 16 | override def reads(json: JsValue): JsResult[Metric] = ??? // not needed 17 | override def writes(o: Metric): JsValue = o match { 18 | case c: Counter => counterJsonFormat.writes(c) 19 | case v: Value => valueJsonFormat.writes(v) 20 | } 21 | } 22 | implicit val formatter = FrameFormatter.jsonFrame[Metric] 23 | 24 | def index() = WebSocket.acceptWithActor[String, Metric] { implicit request => 25 | WebSocketHandlerActor.props 26 | } 27 | } 28 | 29 | object WebSocketHandlerActor { 30 | def props(out: ActorRef) = Props(new WebSocketHandlerActor(out)) 31 | } 32 | 33 | class WebSocketHandlerActor(out: ActorRef) extends Actor { 34 | override def preStart(): Unit = { 35 | router ! AddRoutee(routee) 36 | } 37 | 38 | override def postStop(): Unit = { 39 | router ! RemoveRoutee(routee) 40 | } 41 | 42 | override def receive: Receive = { 43 | case m: Metric => out ! m 44 | } 45 | 46 | private val routee = ActorRefRoutee(self) 47 | private val router = context.system.actorSelection(FlowInitializer.RouterPath) 48 | } 49 | -------------------------------------------------------------------------------- /metrics-collector/app/global/FlowInitializer.scala: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import akka.actor.{PoisonPill, Props} 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 6 | import akka.http.scaladsl.model.StatusCodes.OK 7 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 8 | import akka.http.scaladsl.unmarshalling.Unmarshal 9 | import akka.routing.{RemoveRoutee, NoRoutee, AddRoutee, BroadcastGroup} 10 | import akka.stream.ActorFlowMaterializer 11 | import akka.stream.actor.ActorSubscriberMessage.{OnComplete, OnNext} 12 | import akka.stream.actor.{ActorSubscriber, RequestStrategy, WatermarkRequestStrategy} 13 | import akka.stream.scaladsl._ 14 | import metrics.common.{Value, Counter, Metric} 15 | import play.api._ 16 | import play.api.libs.concurrent.Akka 17 | import reactivemongo.api.MongoDriver 18 | import reactivemongo.api.collections.default.BSONCollection 19 | import reactivemongo.bson.Macros 20 | import reactivemongo.core.nodeset.Authenticate 21 | import scala.collection.immutable.Seq 22 | import scala.concurrent.Future 23 | 24 | object FlowInitializer extends GlobalSettings { 25 | override def onStart(a: Application): Unit = { 26 | implicit val app = a 27 | implicit val actorSystem = Akka.system 28 | implicit val materializer = ActorFlowMaterializer() 29 | implicit val dispatcher = actorSystem.dispatcher 30 | 31 | val interface = app.configuration.getString("metrics-collector.interface").get 32 | val port = app.configuration.getInt("metrics-collector.port").get 33 | 34 | val requestFlow = Flow() { implicit b => 35 | import FlowGraph.Implicits._ 36 | 37 | val wsSubscriber = b.add(Sink.actorSubscriber[Metric](ActorsWs.props)) 38 | val journalerSubscriber = b.add(Sink.actorSubscriber[Metric](ActorJournaler.props(app.configuration))) 39 | 40 | val requestResponseFlow = b.add(Flow[HttpRequest].map(_ => HttpResponse(OK))) 41 | val requestMetricFlow = b.add(Flow[HttpRequest].mapAsync(1) { request => 42 | Unmarshal(request.entity).to[Metric].map(Seq(_)).fallbackTo(Future.successful(Seq.empty[Metric])) 43 | }.mapConcat(identity)) 44 | 45 | val broadcastRequest = b.add(Broadcast[HttpRequest](2)) 46 | val broadcastMetric = b.add(Broadcast[Metric](2)) 47 | 48 | broadcastRequest ~> requestResponseFlow 49 | broadcastRequest ~> requestMetricFlow ~> broadcastMetric ~> wsSubscriber 50 | broadcastMetric ~> journalerSubscriber 51 | 52 | (broadcastRequest.in, requestResponseFlow.outlet) 53 | } 54 | 55 | Http().bindAndHandle(interface = interface, port = port, handler = requestFlow) 56 | 57 | val router = actorSystem.actorOf(BroadcastGroup(List.empty[String]).props(), RouterName) 58 | router ! AddRoutee(NoRoutee) // prevents router from terminating when last websocket disconnects 59 | } 60 | 61 | override def onStop(a: Application): Unit = { 62 | val router = Akka.system(a).actorOf(BroadcastGroup(List.empty[String]).props(), RouterName) 63 | router ! RemoveRoutee(NoRoutee) 64 | router ! PoisonPill 65 | } 66 | 67 | private val RouterName = "BroadcastRouter" 68 | val RouterPath = s"/user/$RouterName" 69 | } 70 | 71 | class ActorsWs extends ActorSubscriber { 72 | override protected def requestStrategy: RequestStrategy = new WatermarkRequestStrategy(1024) 73 | 74 | override def receive: Receive = { 75 | case OnNext(m: Metric) => router ! m 76 | case OnComplete => self ! PoisonPill 77 | } 78 | 79 | private val router = context.system.actorSelection(FlowInitializer.RouterPath) 80 | } 81 | 82 | object ActorsWs { 83 | def props: Props = Props(new ActorsWs) 84 | } 85 | 86 | class ActorJournaler(configuration: Configuration) extends ActorSubscriber { 87 | private val mongoHost = configuration.getString("mongo.host").get 88 | private val mongoDb = configuration.getString("mongo.db").get 89 | private val mongoUser = configuration.getString("mongo.user").get 90 | private val mongoPassword = configuration.getString("mongo.password").get 91 | private implicit val dispatcher = context.dispatcher 92 | private val mongoConnection = (new MongoDriver).connection(nodes = List(mongoHost), authentications = List(Authenticate(mongoDb, mongoUser, mongoPassword))) 93 | private val mongoDatabase = mongoConnection(mongoDb) 94 | private val metrics: BSONCollection = mongoDatabase("metrics") 95 | implicit val counterMongoHandler = Macros.handler[Counter] 96 | implicit val valueMongoHandler = Macros.handler[Value] 97 | 98 | override protected def requestStrategy: RequestStrategy = new WatermarkRequestStrategy(1024) 99 | 100 | override def receive: Receive = { 101 | case OnNext(c: Counter) => metrics.insert(c) 102 | case OnNext(v: Value) => metrics.insert(v) 103 | case OnComplete => self ! PoisonPill 104 | } 105 | } 106 | 107 | object ActorJournaler { 108 | def props(configuration: Configuration): Props = Props(new ActorJournaler(configuration)) 109 | } -------------------------------------------------------------------------------- /metrics-collector/conf/application.conf: -------------------------------------------------------------------------------- 1 | application.global = global.FlowInitializer 2 | 3 | logger { 4 | root = DEBUG 5 | play = DEBUG 6 | application = DEBUG 7 | } 8 | 9 | metrics-collector { 10 | interface = "0.0.0.0" 11 | port = 5000 12 | } 13 | 14 | mongo { 15 | host = "localhost" 16 | db = "metrics-collector" 17 | user = "" 18 | password = "" 19 | } -------------------------------------------------------------------------------- /metrics-collector/conf/routes: -------------------------------------------------------------------------------- 1 | GET /metrics controllers.MetricsCollector.index() -------------------------------------------------------------------------------- /metrics-common/src/main/scala/metrics/common/Metrics.scala: -------------------------------------------------------------------------------- 1 | package metrics.common 2 | 3 | import akka.actor._ 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.client.RequestBuilding 6 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 7 | import akka.stream.FlowMaterializer 8 | import akka.stream.actor.ActorPublisher 9 | import akka.stream.actor.ActorPublisherMessage.{Cancel, Request} 10 | import akka.stream.scaladsl.{RunnableFlow, Flow, Sink, Source} 11 | import com.typesafe.config.Config 12 | import spray.json.{DefaultJsonProtocol, JsValue, RootJsonFormat, _} 13 | 14 | import scala.annotation.tailrec 15 | import scala.concurrent.ExecutionContextExecutor 16 | import scala.util.Try 17 | 18 | sealed trait Metric { 19 | val path: String 20 | val timestamp: Long 21 | } 22 | 23 | case class Counter(override val path: String, count: Long, override val timestamp: Long = System.currentTimeMillis()) extends Metric 24 | 25 | object Counter extends DefaultJsonProtocol { 26 | implicit val format = jsonFormat3(Counter.apply) 27 | } 28 | 29 | case class Value(override val path: String, value: Long, override val timestamp: Long = System.currentTimeMillis()) extends Metric 30 | 31 | object Value extends DefaultJsonProtocol { 32 | implicit val format = jsonFormat3(Value.apply) 33 | } 34 | 35 | object Metric extends DefaultJsonProtocol { 36 | implicit val format = new RootJsonFormat[Metric] { 37 | override def write(metric: Metric): JsValue = { 38 | metric match { 39 | case counter: Counter => counter.toJson 40 | case value: Value => value.toJson 41 | } 42 | } 43 | 44 | override def read(json: JsValue): Metric = { 45 | Try(json.convertTo[Counter]).getOrElse(json.convertTo[Value]) 46 | } 47 | } 48 | } 49 | 50 | trait Metrics { 51 | protected implicit val actorSystem: ActorSystem 52 | protected implicit val dispatcher: ExecutionContextExecutor 53 | protected implicit val materializer: FlowMaterializer 54 | protected val config: Config 55 | 56 | private lazy val metricsConnectionFlow = Http().outgoingConnection(config.getString("services.metrics-collector.host"), 57 | config.getInt("services.metrics-collector.port")) 58 | private lazy val metricsSource = Source.actorPublisher[Metric](MetricsManager.props) 59 | private lazy val requestFlow = Flow[Metric].map(m => RequestBuilding.Post("/metrics", m)) 60 | private lazy val metricsFlow: RunnableFlow[ActorRef] = metricsSource.via(requestFlow).via(metricsConnectionFlow).to(Sink.onComplete { _ => 61 | val metricsManagerRef = metricsFlow.run() 62 | metricsSupervisorRef ! MetricsSupervisor.NewMetricsManager(metricsManagerRef) 63 | }) 64 | 65 | private lazy val metricsManagerRef = metricsFlow.run() 66 | 67 | private lazy val metricsSupervisorRef = actorSystem.actorOf(MetricsSupervisor.props(metricsManagerRef)) 68 | 69 | def putMetric(metric: Metric): Unit = metricsSupervisorRef ! metric 70 | 71 | private class MetricsSupervisor(initial: ActorRef) extends Actor { 72 | override def receive: Receive = { 73 | case m: Metric => metricsManager ! m 74 | case MetricsSupervisor.NewMetricsManager(mm) => metricsManager = mm 75 | } 76 | 77 | private var metricsManager = initial 78 | } 79 | 80 | private object MetricsSupervisor { 81 | def props(initial: ActorRef) = Props(new MetricsSupervisor(initial)) 82 | 83 | case class NewMetricsManager(metricsManager: ActorRef) 84 | } 85 | 86 | private class MetricsManager extends ActorPublisher[Metric] { 87 | override def receive: Receive = { 88 | case metric: Metric if buffer.size == MaxBufferSize => 89 | // drop 90 | case metric: Metric => 91 | if (buffer.isEmpty && totalDemand > 0) { 92 | onNext(metric) 93 | } else { 94 | buffer :+= metric 95 | deliverBuffer() 96 | } 97 | case Request(n) => 98 | deliverBuffer() 99 | case Cancel => context.stop(self) 100 | } 101 | 102 | @tailrec 103 | private def deliverBuffer(): Unit = { 104 | if (totalDemand > 0) { 105 | if (totalDemand < Int.MaxValue) { 106 | val (use, keep) = buffer.splitAt(totalDemand.toInt) 107 | buffer = keep 108 | use.foreach(onNext) 109 | } else { 110 | val (use, keep) = buffer.splitAt(Int.MaxValue) 111 | buffer = keep 112 | use.foreach(onNext) 113 | deliverBuffer() 114 | } 115 | } 116 | } 117 | } 118 | 119 | private object MetricsManager { 120 | def props: Props = Props(new MetricsManager) 121 | } 122 | 123 | private val MaxBufferSize = 1024 124 | private var buffer = Vector.empty[Metric] 125 | } -------------------------------------------------------------------------------- /metrics-common/src/main/scala/metrics/common/MetricsDirectives.scala: -------------------------------------------------------------------------------- 1 | package metrics.common 2 | 3 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 4 | import akka.http.scaladsl.server.Directive0 5 | 6 | case class RequestResponseStats(request: HttpRequest, response: HttpResponse, time: Long) 7 | 8 | trait MetricsDirectives { 9 | import akka.http.scaladsl.server.directives.BasicDirectives._ 10 | 11 | def measureRequestResponse(f: (RequestResponseStats => Unit)): Directive0 = { 12 | extractRequestContext.flatMap { ctx => 13 | val start = System.currentTimeMillis() 14 | mapResponse { response => 15 | val stop = System.currentTimeMillis() 16 | f(RequestResponseStats(ctx.request, response, stop - start)) 17 | response 18 | } 19 | } 20 | } 21 | } 22 | 23 | object MetricsDirectives extends MetricsDirectives -------------------------------------------------------------------------------- /postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres 2 | 3 | ADD init.sql /docker-entrypoint-initdb.d/ 4 | ADD identity.sql /docker-entrypoint-initdb.d/ 5 | ADD auth_entry.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /postgres/auth_entry.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE auth_password; 2 | \connect auth_password; 3 | CREATE TABLE "auth_entry"( 4 | id SERIAL PRIMARY KEY, 5 | identity_id BIGINT NOT NULL, 6 | created_at BIGINT NOT NULL, 7 | email VARCHAR(50) NOT NULL UNIQUE, 8 | password VARCHAR(255) NOT NULL 9 | ) -------------------------------------------------------------------------------- /postgres/identity.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE identity_manager; 2 | \connect identity_manager; 3 | CREATE TABLE "identity"( 4 | id SERIAL PRIMARY KEY, 5 | created_at BIGINT NOT NULL 6 | ) 7 | -------------------------------------------------------------------------------- /postgres/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE auth_codecard; 2 | \connect auth_codecard; 3 | CREATE TABLE "auth_entry"( 4 | user_identifier CHAR(10) PRIMARY KEY, 5 | identity_id BIGINT NOT NULL, 6 | created_at BIGINT NOT NULL, 7 | last_card BIGINT 8 | ); 9 | 10 | CREATE TABLE "code"( 11 | user_identifier CHAR(10) REFERENCES auth_entry, 12 | card_index BIGINT NOT NULL, 13 | code_index BIGINT NOT NULL, 14 | code VARCHAR(6) NOT NULL, 15 | created_at BIGINT NOT NULL, 16 | activated_at BIGINT, 17 | used_at BIGINT, 18 | PRIMARY KEY (user_identifier, card_index, code_index) 19 | ); -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" 2 | 3 | resolvers += "rediscala" at "http://dl.bintray.com/etaty/maven" 4 | 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.7") 6 | 7 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.7.2") 8 | -------------------------------------------------------------------------------- /session-manager/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8011 8 | } 9 | 10 | services { 11 | token-manager { 12 | host = "localhost" 13 | port = 8010 14 | } 15 | } -------------------------------------------------------------------------------- /session-manager/src/main/scala/SessionManager.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.client.RequestBuilding 4 | import akka.http.scaladsl.model.{HttpResponse, HttpRequest} 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.stream.ActorFlowMaterializer 7 | import akka.stream.scaladsl.{Sink, Source} 8 | import com.typesafe.config.ConfigFactory 9 | import scala.concurrent.Future 10 | 11 | object SessionManager extends App { 12 | val config = ConfigFactory.load() 13 | val interface = config.getString("http.interface") 14 | val port = config.getInt("http.port") 15 | val tokenManagerHost = config.getString("services.token-manager.host") 16 | val tokenManagerPort = config.getInt("services.token-manager.port") 17 | 18 | implicit val actorSystem = ActorSystem() 19 | implicit val materializer = ActorFlowMaterializer() 20 | implicit val dispatcher = actorSystem.dispatcher 21 | 22 | val tokenManagerConnectionFlow = Http().outgoingConnection(tokenManagerHost, tokenManagerPort) 23 | 24 | def requestTokenManager(request: HttpRequest): Future[HttpResponse] = { 25 | Source.single(request).via(tokenManagerConnectionFlow).runWith(Sink.head) 26 | } 27 | 28 | Http().bindAndHandle(interface = interface, port = port, handler = { 29 | logRequestResult("session-manager") { 30 | path("session") { 31 | headerValueByName("Auth-Token") { tokenValue => 32 | pathEndOrSingleSlash { 33 | get { 34 | complete { 35 | requestTokenManager(RequestBuilding.Get(s"/tokens/$tokenValue")) 36 | } 37 | } ~ 38 | delete { 39 | complete { 40 | requestTokenManager(RequestBuilding.Delete(s"/tokens/$tokenValue")) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /token-manager/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } 4 | 5 | http { 6 | interface = "0.0.0.0" 7 | port = 8010 8 | } 9 | 10 | session { 11 | ttl = 900 # seconds 12 | } 13 | 14 | token { 15 | ttl = 2592000 # seconds 16 | } 17 | 18 | mongo { 19 | host = "localhost" 20 | db = "token-manager" 21 | user = "" 22 | password = "" 23 | } 24 | 25 | services { 26 | metrics-collector { 27 | host = "localhost" 28 | port = 5000 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /token-manager/src/main/scala/Config.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.config.ConfigFactory 2 | 3 | trait Config { 4 | protected val config = ConfigFactory.load() 5 | protected val interface = config.getString("http.interface") 6 | protected val port = config.getInt("http.port") 7 | protected val mongoHost = config.getString("mongo.host") 8 | protected val mongoDb = config.getString("mongo.db") 9 | protected val mongoUser = config.getString("mongo.user") 10 | protected val mongoPassword = config.getString("mongo.password") 11 | protected val sessionTtl = config.getLong("session.ttl") * 1000 // milliseconds 12 | protected val tokenTtl = config.getLong("token.ttl") * 1000 // milliseconds 13 | } 14 | -------------------------------------------------------------------------------- /token-manager/src/main/scala/JsonProtocols.scala: -------------------------------------------------------------------------------- 1 | import spray.json.DefaultJsonProtocol 2 | 3 | trait JsonProtocols extends DefaultJsonProtocol { 4 | protected implicit val tokenFormat = jsonFormat4(Token.apply) 5 | protected implicit val reloginRequestFormat = jsonFormat2(ReloginRequest.apply) 6 | protected implicit val loginRequestFormat = jsonFormat2(LoginRequest.apply) 7 | } -------------------------------------------------------------------------------- /token-manager/src/main/scala/Repository.scala: -------------------------------------------------------------------------------- 1 | import reactivemongo.api.MongoDriver 2 | import reactivemongo.api.collections.default.BSONCollection 3 | import reactivemongo.bson.{Macros, BSONDocument} 4 | import reactivemongo.core.nodeset.Authenticate 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | class Repository(implicit ec: ExecutionContext) extends Config { 8 | def insertToken(token: Token): Future[Token] = tokens.insert(token).map(_ => token) 9 | 10 | def updateTokenByValue(value: String, token: Token): Future[Int] = tokens.update(BSONDocument("value" -> value), token).map(_.updated) 11 | 12 | def deleteTokenByValue(value: String): Future[Int] = tokens.remove(BSONDocument("value" -> value)).map(_.updated) 13 | 14 | def findValidTokenByValue(value: String): Future[Option[Token]] = { 15 | tokens.find(BSONDocument("value" -> value, "validTo" -> BSONDocument("$gt" -> System.currentTimeMillis()))).cursor[Token].headOption 16 | } 17 | 18 | def addMethodToValidTokenByValue(value: String, method: String): Future[Option[Token]] = { 19 | tokens.update(BSONDocument("value" -> value), BSONDocument("$addToSet" -> BSONDocument("authMethods" -> method))).flatMap { lastError => 20 | if (lastError.updated > 0) findValidTokenByValue(value) else Future.successful(None) 21 | } 22 | } 23 | 24 | private implicit val tokenHandler = Macros.handler[Token] 25 | private val mongoConnection = (new MongoDriver).connection(nodes = List(mongoHost), authentications = List(Authenticate(mongoDb, mongoUser, mongoPassword))) 26 | private val mongoDatabase = mongoConnection(mongoDb) 27 | private val tokens: BSONCollection = mongoDatabase("tokens") 28 | } 29 | -------------------------------------------------------------------------------- /token-manager/src/main/scala/Service.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.event.Logging 3 | import java.math.BigInteger 4 | import java.security.SecureRandom 5 | import scala.concurrent.{ExecutionContext, Future} 6 | 7 | class Service(repository: Repository)(implicit actorSystem: ActorSystem, ec: ExecutionContext) extends Config { 8 | def relogin(reloginRequest: ReloginRequest): Future[Option[Token]] = { 9 | repository.addMethodToValidTokenByValue(reloginRequest.tokenValue, reloginRequest.authMethod) 10 | } 11 | 12 | def login(loginRequest: LoginRequest): Future[Token] = { 13 | val newToken = createFreshToken(loginRequest.identityId, loginRequest.authMethod) 14 | repository.insertToken(newToken).map(_ => newToken) 15 | } 16 | 17 | def findAndRefreshToken(tokenValue: String): Future[Option[Token]] = { 18 | repository.findValidTokenByValue(tokenValue).map { tokenOption => 19 | tokenOption.map { token => 20 | val newToken = refreshToken(token) 21 | if (newToken != token) 22 | repository.updateTokenByValue(token.value, newToken).onFailure { case t => logger.error(t, "Token refreshment failed") } 23 | newToken 24 | } 25 | } 26 | } 27 | 28 | def logout(tokenValue: String): Unit = { 29 | repository.deleteTokenByValue(tokenValue).onFailure { case t => logger.error(t, "Token deletion failed") } 30 | } 31 | 32 | private def createFreshToken(identityId: Long, authMethod: String): Token = { 33 | Token(generateToken, System.currentTimeMillis() + tokenTtl, identityId, Set(authMethod)) 34 | } 35 | 36 | private def generateToken: String = new BigInteger(255, random).toString(32) 37 | 38 | private def refreshToken(token: Token): Token = token.copy(validTo = math.max(token.validTo, System.currentTimeMillis() + sessionTtl)) 39 | 40 | private val random = new SecureRandom() 41 | 42 | private val logger = Logging(actorSystem, getClass) 43 | } 44 | -------------------------------------------------------------------------------- /token-manager/src/main/scala/TokenManager.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ 4 | import akka.http.scaladsl.marshalling.ToResponseMarshallable 5 | import akka.http.scaladsl.model.StatusCodes._ 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.stream.ActorFlowMaterializer 8 | import metrics.common.{Value, RequestResponseStats, Counter, Metrics} 9 | import metrics.common.MetricsDirectives._ 10 | 11 | case class LoginRequest(identityId: Long, authMethod: String) 12 | 13 | case class ReloginRequest(tokenValue: String, authMethod: String) 14 | 15 | case class Token(value: String, validTo: Long, identityId: Long, authMethods: Set[String]) 16 | 17 | object TokenManager extends App with JsonProtocols with Config with Metrics { 18 | implicit val actorSystem = ActorSystem() 19 | implicit val materializer = ActorFlowMaterializer() 20 | implicit val dispatcher = actorSystem.dispatcher 21 | 22 | val repository = new Repository 23 | val service = new Service(repository) 24 | 25 | def putMetricForRequestResponse(requestStats: RequestResponseStats): Unit = { 26 | val method = requestStats.request.method.name.toLowerCase 27 | putMetric(Value(s"token-manager.$method.time", requestStats.time)) 28 | } 29 | 30 | Http().bindAndHandle(interface = interface, port = port, handler = { 31 | (measureRequestResponse(putMetricForRequestResponse) & logRequestResult("token-manager")) { 32 | pathPrefix("tokens") { 33 | (post & pathEndOrSingleSlash & entity(as[LoginRequest])) { loginRequest => 34 | complete { 35 | putMetric(Counter("token-manager.post", 1)) 36 | service.login(loginRequest).map(token => Created -> token) 37 | } 38 | } ~ 39 | (patch & pathEndOrSingleSlash & entity(as[ReloginRequest])) { reloginRequest => 40 | complete { 41 | service.relogin(reloginRequest).map[ToResponseMarshallable] { 42 | case Some(token) => 43 | putMetric(Counter("token-manager.patch", 1)) 44 | OK -> token 45 | case None => 46 | putMetric(Counter("token-manager.patch", -1)) 47 | NotFound -> "Token expired or not found" 48 | } 49 | } 50 | } ~ 51 | (path(Segment) & pathEndOrSingleSlash) { tokenValue => 52 | get { 53 | complete { 54 | service.findAndRefreshToken(tokenValue).map[ToResponseMarshallable] { 55 | case Some(token) => 56 | putMetric(Counter("token-manager.get", 1)) 57 | OK -> token 58 | case None => 59 | putMetric(Counter("token-manager.get", -1)) 60 | NotFound -> "Token expired or not found" 61 | } 62 | } 63 | } ~ 64 | delete { 65 | complete { 66 | service.logout(tokenValue) 67 | putMetric(Counter("token-manager.delete", 1)) 68 | OK 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }) 75 | } 76 | 77 | -------------------------------------------------------------------------------- /tutorial/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theiterators/reactive-microservices/467e38e93ca1c1258e37415a079da8b9a3410cb7/tutorial/arch.png -------------------------------------------------------------------------------- /tutorial/btc-ws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theiterators/reactive-microservices/467e38e93ca1c1258e37415a079da8b9a3410cb7/tutorial/btc-ws.png -------------------------------------------------------------------------------- /tutorial/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theiterators/reactive-microservices/467e38e93ca1c1258e37415a079da8b9a3410cb7/tutorial/flow.png -------------------------------------------------------------------------------- /tutorial/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Authentication system based on a microservices architecture 5 | 6 | 7 |
8 |

1. Reactive microservices

9 | 10 |

11 | Reactive microservices is an activator template completely devoted to microservices architecture. It lets you learn about microservices in general — different patterns, communication protocols and 'tastes' of microservices. All these concepts are demonstrated using Scala, Akka, Play and other tools from Scala ecosystem. For the sake of clarity, we skipped topics related to deployment and operations — that's a great subject for another big activator template. 12 |

13 | 14 |

15 |

21 |

22 |
23 |
24 |

1.1. Prerequisites

25 | 26 |

27 | To feel comfortable while playing with this template, make sure you know basics of Akka HTTP which is a cornerstone of this project. We recently released an Akka HTTP activator template that may help you start. At least brief knowledge of Akka remoting, Akka persistence, Akka streams and Play Framework websockets is also highly recommended. Anyway, don't worry — all these technologies (and more!) will be discussed on the way but we won't dig into the details. 28 |

29 | 32 |
33 |
34 |

1.2. Project build overview

35 |

36 | This activator template consists of 9 runnable subprojects — the microservices: 37 |

63 | They uses different communication methods, different databases, and different frameworks. 64 |

65 | 68 |
69 |
70 |

1.3. Setup instructions

71 | 72 |

Review the configuration files

73 |

Take some time to review application.conf files that are located in resource subdirectory of each microservice. You can also look at docker-compose.yml file, which contains docker preconfigurated images for all the required databases.

74 | 75 |

Run migrations (You don't need to do this step if you want to use our docker container)

76 |

For auth-codecard, identity-manager and auth-password you need to run the SQL migration scripts which are located in postgres directory. If you want to use non-default names please tweak the `application.conf` files. 77 | You can also tweak and use this script in your console: 78 | 79 |
80 | cd /where/this/activator/template/is/located/
81 | psql -h localhost -U postgres -f ./postgres/init.sql &&
82 | psql -h localhost -U postgres -f ./postgres/auth_entry.sql &&
83 | psql -h localhost -U postgres -f ./postgres/identity.sql
84 |

85 | 86 |

Running

87 |

Run docker-compose up in project main directory to launch databases, or if you are using your own database instances, make sure you have PostgreSQL, MongoDB and Redis up and running.

88 | 89 |

Akka HTTP

90 |

You can run each service separately, but we also we provided a SBT task called runAll.

91 | 92 |

Play

93 |

Due to some issues with Play/sbt cooperation metrics-collector and btc-ws should be run separately. 94 | In order to run them in one sbt CLI instance use these commands:
95 | ; project btc-ws; run 9000
96 | ; project metrics-collector; run 5001
97 | Everything else should work out of the box. Enjoy! 98 |

99 | 100 | 104 |
105 |
106 |

2. What are microservices?

107 | 108 |

109 | There's no formal or widespread definition of a microservice. However usually microservices are defined as an software architectural style in which system is composed of multiple services. Those services are small (or at least smaller than in typical, monolith applications), can be independently deployed and they communicate using (lightweight) protocols. Well–defined microservice should be organized around business capabilities or to put it in Domain–Driven Design wording — should encapsulate Bounded Context. Sometimes it is said that microservices are implementation of Single Responsibility Principle in architecture. 110 |

111 | 112 | 115 |
116 |
117 |

2.1. Opportunities

118 | 119 |

120 | Applications based on a microservice architecture are basically distributed systems. This means that they can scale horizontally in a much more flexible way than monolithic systems — instead of replicating whole heavyweight process one can spawn multiple instances of services that are under load. This guarantees better hardware utilization — money savings. Another important consequence of moving from monolith to microservices is the need of designing for failure which can result in a truly reactive system. Like it or not, while designing a distributed system you have to take failure into account — otherwise you will see your system falling apart. 121 |

122 |

123 | However, technical benefits of introducing a microservice–based architecture are far less important than the social ones. MSA enables truly cross–functional teams organized around business capabilities which are implemented by microservices. This in turn allows easy team scaling and makes team members care more about actual business behind the technology. This agility together with autonomy of teams usually result in a shorter time–to–market. In fact, most of the early adopters of microservices (Netflix, Amazon, SoundCloud, Tumblr) underline the ability to deliver faster as the main selling point of microservices. 124 |
125 | Shorter time–to–market stems not only from well–organized teams but also from other features of MSA. First of all, microservices are supposed to be easier to understand (thus maintain) than monolithic systems. Smaller size also means that microservices can be easily rewritten (or simply disposed) instead of being refactored which usually is expansive and results in sub–optimal code quality. Autonomy of teams enables polyglot approach which provides better utilization of tools and bounded context is supposed to increase code reusability. Microservices should also be fun for developers, as everything that's new and challenging. 126 |

127 | 130 |
131 |
132 |

2.2. Dilemmas and problems

133 | 134 |

135 | A shift from monolithic to microservices architecture is a serious step. Distributed systems are totally different than monolithic ones and have their own dilemmas and problems. First and foremost microservices are all about communication and protocols. One should be aware that microservices doesn't magically suppress complexity — they just move it from code to communication layer. Different communication protocols (synchronous and asynchronous) and transport guarantees will be the main subject of this tutorial (see chapters 3.1., 3.2., 3.3., 3.4.). Another important subject worth mentioning while discussing microservices is a polyglot persistence — how to embrace multiple different data stores and not lose consistency and performance. You can check out this approach in our activator template in chapter 3. Very common question asked while developing microservices is 'how big is a microservice?' — we'll discuss it in chapter 3. Microservice approach requires some boilerplate; one may be tempted to share code using shared libraries which introduce coupling — why and when would you like to do that? See chapter 3.1. There's also a multitude of the problems which are out of the scope of this template like: testing (why, when and how to do it?), polyglot approach (is it worth the cost?), operations, contract management (what's my API, who are my collaborators, how can I contact them?), API versioning (how to stay backward compatible?), logging & debugging and security. Feel encouraged to enrich this activator template with suitable examples and tutorials — and let us know! 136 |

137 | 138 | 141 |
142 |
143 |

3. System blueprint

144 | 145 |

146 | To present different concepts related to microservices we built an authentication system. The idea behind it is really simple — you can sign in/sign up using arbitrarily chosen authentication methods (currently they're email–password, Facebook Oauth, codecard). Number of used authentication methods indicates user's token strength. User that presents a valid authentication token can access business applications behind the authentication system. To test if our authentication system actually works we integrated a simple application that after singing in lets users subscribe and get notifications in real–time about bitcoin market events such as rate change, volume above/below certain level etc. 147 |

148 | 149 |

System design

150 | 151 |

If you want to have a closer look at this schema go here.

152 |

153 | Complete system consists of 9 microservices: 154 |

165 | 166 | Spend some time analyzing communication paths, protocols and data stores — it might be useful during our tour around each service of the system. 167 |

168 | 169 | 175 | 176 |
177 |
178 |

3.1. Synchronous HTTP — akka–http

179 |

180 | Synchronous communication is a data transfer method in which receiving and sending are governed by strict timing signals. It's usually takes the form of a request–response protocol — party sends data only when it's explicitly asked for it. That's how typical HTTP service works. Synchronous protocols are very popular because they're easy to understand and analyze. However they have one significant drawback — they don't scale well. First of all, synchronous protocols introduce liveness issues — when you ask for something you have to be prepared that your call may explicitly fail or even worse you may never get any response at all (that's why you need timeouts). Secondly, usually if you ask for something, you have to wait for the response to continue processing. Several such calls and you'll end up waiting most of the time instead of doing something actually useful. Having said that, world wide web we use daily is mostly synchronous — you click 'Log in' button and you wait for 'Login successful' confirmation box. Request–response — that's how vanilla HTTP works. Synchronous communication is also useful (well, almost unavoidable) while designing microservice–based system — learn why and when. 181 |

182 |

identity-manager

183 |

184 | identity-manager is a microservice for issuing and managing existing identities in the system. Code of this service is very straightforward, it consists of: 185 |

192 | identity-manager has well–defined responsibility — creating and listing existing identities. For now it only keep identity id and creation date but as such system evolves it might be useful to keep ex. user's first and last name (feel encouraged to create this enhancement!). The service is really short (75 lines!), thus easy to understand and maintain. It doesn't have well–known layer structure (repository–service–routing) as it would only introduce burden and additional boilerplate. That's however not always the case — let's see more complex services. 193 |

194 | 195 |

auth-codecard, auth-password

196 |

197 | auth-codecard, auth-password are services for signing in/signing up using codecard and email–password respectively. These are user facing services, which means that SPA frontend application communicates with them directly. They're synchronous by design — when user logs in or registers, she has to wait for the operation to complete before proceeding. These services are much more complex, thus structured: 198 |

206 | There are a few things to note about these services. First of all, they provide similar functionalities but each authentication method has its own characteristics (ex. codecard needs card renewal, email–password needs password changing) — that's why they're completely separated. Another important thing is that structure introduces organizational complexity but abstraction allows to hide some implementation details and makes code easier to understand and maintain. Last but not least, careful reader might have noticed that both Gateway source codes looks really similar (codecard, password). One may be tempted to write a shared library to reduce source code duplication. Shared libraries are generally discouraged when writing microservices because they introduce another layer of coupling. However, that's sometimes unavoidable — we'll see that in the next chapter. 207 |

208 | 209 |

auth-fb

210 |

211 | auth-fb is a service for signing in/signing up with Facebook Oauth and is very similar to auth-codecard and auth-password. However it misses separate Repository layer. Microservices are all about agility and flexibility, so we don't have to follow the same rules all the time. auth-fb uses Redis — database with very simple interface and writing dedicated class for interacting with it would be an overkill. That's why all the database access is done inside Service. Looking at auth-fb it's easy to notice how polyglot persistence approach is helping developing microservices — each authentication method service is responsible for (and only for) its own registration data and can use the best persistence method to store it (simple key–value store in case of auth-fb). So called 'distributed truth' makes systems little harder to maintain and use (you can't query all the datastores at once, data access is non–uniform etc.) but it enables better tools utilization, scaling, decoupling and resilience (of course when done right). 212 |

213 | 214 |

token-manager

215 |

216 | token-manager is a focal point of our authentication system. Authentication method services gets a fresh token for logged in users from it and business services verifies tokens presented by users to check identities. token-manager is built using previously presented layered architecture (routes — service — repository) but it misses gateway as it doesn't initiate communication with other services. It's the most important service in whole project so it's equipped with custom metrics reporting — there's processing time reporting for every request and success/failure reporting for each action. You'll learn how it works under the hood in the next chapter. 217 |

218 | 219 |

session-manager

220 |

221 | In the microservice architecture you usually have services that offer public API accessible by clients and internal API that is being used only by other services. Sometimes service offers some features that are public and others that are strictly private. That's the case with token-manager — you want your clients to be able to log out (delete token) but you don't want them to be able to add new token without going through one of auth services. This could be easily handled by a proxy server (like HAProxy or nginx) but sometimes you may want to do more complex transformations (ex. change API). In this case you should write a proxy service. Session manager is an exemplary proxy service that changes API and hides internals. 222 |

223 | 226 |
227 |
228 |

3.2. Asynchronous HTTP stream — akka–http

229 |

230 | Asynchronous communication, unlike synchronous, is not restricted by any timing signals. Asynchronous communication is usually implemented by message passing (vs request–response) — 'telling' (vs 'asking'). Asynchronous protocols usually scales better as communicating parties don't have to wait. However asynchronous message passing is unnatural, can get really complex thus it's very often complicated and hard to debug. Nonetheless, truly reactive systems should relay on asynchronous message–passing — as stated in Reactive Manifesto. 231 |

232 | 233 |

metrics-common

234 |

235 | In the previous chapter you saw nice interface for reporting metrics. Let's see how it's built. metrics-common is a shared library that consists of: 236 |

249 | One may argue that this approach isn't asynchronous because we're using well–known HTTP requests. That's not true at all — we're using long–lived HTTP connection which provides us with an efficient stream and what's even more important, we're issuing requests without waiting for responses — we explicitly ignore them! This makes HTTP a one–way message stream and enables asynchronous processing.
250 | Another thing worth noting is fact we implemented metrics via a shared library that we declared a bad practise before because it introduces coupling. That's true but having nice and clean interface for reporting metrics simply outweighs the drawbacks. Still, if author of another microservice don't like that or don't use Scala, she can communicate with metrics-collector using HTTP and JSON, completely ignoring metrics-common library. 251 |

252 | 253 |

metrics-collector

254 |

255 | metrics-collector is the receiving end of metrics subsystem. It's a Play app that receives metrics, stores them in a database and presents them to system administrators via websockets. Websockets part is really straightforward — it passes received metrics to websocket. The receiving endpoint is much more interesting. It's of course built using Akka HTTP but with customized flow instead of typical routing. The flow starts with broadcasting to requestResponseFlow and requestMetricFlow. requestResponseFlow simply maps every request to 200 HTTP response. requestMetricFlow turns requests into Metrics or in case of error to 'empty' element. requestResponseFlow is the actual output of the main flow while requestMetricFlow is being broadcasted to wsSubscriber and journalerSubscriber. journalerSubscriber is an actor subscriber that saves received metrics to MongoDB (ex. for further analysis). wsSubscriber is another actor subscriber that broadcasts received metrics to all connected websockets via Akka router. 256 |

257 | 258 |

If you want to have a closer look at this schema go here.

259 | 260 | 263 |
264 |
265 |

3.3. Asynchronous HTTP — websockets in Play

266 |

267 | Another way to provide asynchronicity in the world of webservers are websockets. Websockets are similar to TCP sockets but additionally they provide minimal framing. This makes them ideal for asynchronous message passing. You've seen them in action in metrics-collector but this time we'll analyse more complex service. 268 |

269 | 270 |

btc-ws

271 |

272 | btc-ws is a façade service for business part of our application. It allows users, after authentication, to manage subscriptions for BTC market events and receive alerts. While one could argue that user's actions can be handled synchronously, market events are asynchronous and should be handled like that. Websockets are perfect fit for such case — otherwise we would have to use HTTP polling which is inefficient. First thing to notice in the btc-ws code is a long block of mappings needed for a websocket message — scala object translations. Websocket initialization code is really straightforward — it retrieves user's identity based on presented token and opens websocket handled by WebSocketHandler actor. You'll learn what happens there in the next chapter. 273 |

274 | 275 | 278 |
279 |
280 |

3.4. Asynchronous messaging — Akka actors

281 |

282 | The 'go–to' tool when it comes to asynchronous message passing in Scala world is of course Akka. It has great capabilities and convenient interface but it comes with a cost. If you chose Akka as a communication protocol for a significant part of your project you're losing many of the benefits of polyglot approach. First of all, if you want to interoperate with Akka, you have to be on JVM and preferably use Java or Scala. Your code may also become more tightly coupled — Akka encourages usage of shared libraries and data structures. As a result, in certain cases it might be better to consider using lightweight message queues such as RabbitMQ or Kafka to avoid aforementioned drawbacks but if you're sure you won't be leaving JVM anytime soon, Akka is definitely the best choice. That was also our decision in the fully asynchronous part of our system. 283 |

284 | 285 |

btc-ws continued

286 |

287 | WebSocketHandler actor is modelled as a simple state machine to handle all the possible failures. Let's see a state diagram. 288 |

289 | 290 |

291 | When websocket connects and actor starts it sends a request to btc-users supervisor to get a remote command handler (ActorRef) (waitForHandler state), in case of failure it simply disconnects websocket — there's nothing we can do more here. After successful handler acquiring (waitForSubscriptions state), websocket actor requests a list of existing subscriptions for user — handler failure is the same as before. List of subscriptions causes websocket actor to enter operational handling of commands (which are routed to handler) and market events from handler (which are routed to websocket) — handleUser state; in case of timeout we assume something bad happened to handler so we switch back to waitForHandler state. Notice how clean protocol we've got — btc-ws microservice doesn't know any implementation details of btc-users — it just sends messages with clear semantics. 292 |

293 | 294 |

btc-users

295 |

296 | btc-users is a microservice completely based on Akka. It consists of three actors types: UsersManager, UserHandler and DataFetcher. UsersManager plays a role of supervisor. It responds to requests from btc-ws to create a handler (UserHandler actor) for user with given id. UserHandler is where all the heavy lifting happens. It's a persistent actor that processes subscription requests and issues alarms based on ticker from BTC market. First and foremost we once again leveraged the polyglot persistence approach — we used Akka persistence to persist subscription settings. Subscribe/unsubscribe actions are a perfect cases for event sourcing and that's exactly how we implemented it — see receiveCommand and receiveRecover. Besides handling subscribe/unsubscribe requests, UserHandler responses to QuerySubscriptions and broadcasts market alarms. UserHandler actor manages its lifetime similar to how btc-ws does — by heartbeats and timeouts. DataFetcher is a very simple actor that every few seconds fetches BTC ticker and broadcasts it to all UserHandler actors via Akka router. That's it — that's how subscriptions are managed and market alarms are issued. 297 |

298 | 299 | 302 |
303 |
304 |

4. Summary

305 |

306 | During our tour of microservices we hopefully showed the full power that comes with mixing different approaches, techniques and tools. Scala's toolbelt (and especially Akka and Play) is, without a question, ready for building reactive microservice–based distributed systems. However before migrating all your project to MSA make sure you deeply understand all dilemmas and problems of microservices, particularly ones we had to omit to keep this activator template concise like: eventual consistency, testing, operations & deployment, contract managing, versioning, monitoring, logging & debugging and security. Good luck, have fun and let us know about your adventures on the microservice way! 307 |

308 | 309 |
310 | 311 | 312 | --------------------------------------------------------------------------------