party cache impl, fix reject handling, correct party creation api

This commit is contained in:
Evgenii Alekseev 2019-11-02 14:41:36 +03:00
parent e03f8987b0
commit 50acecd97e
25 changed files with 291 additions and 187 deletions

View File

@ -11,6 +11,6 @@
<logger name="http" level="DEBUG"> <logger name="http" level="DEBUG">
<appender-ref ref="http" /> <appender-ref ref="http" />
</logger> </logger>
<logger name="slick.jdbc.JdbcBackend.statement" level="DEBUG" /> <logger name="slick" level="INFO" />
</configuration> </configuration>

View File

@ -42,6 +42,8 @@ me.arcanis.ffxivbis {
] ]
# general request timeout, duratin, required # general request timeout, duratin, required
request-timeout = 10s request-timeout = 10s
# party in-memory storage lifetime
cache-timeout = 1m
} }
web { web {

View File

@ -13,7 +13,7 @@ import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.RootEndpoint import me.arcanis.ffxivbis.http.RootEndpoint
import me.arcanis.ffxivbis.service.Ariyala import me.arcanis.ffxivbis.service.{Ariyala, PartyService}
import me.arcanis.ffxivbis.service.impl.DatabaseImpl import me.arcanis.ffxivbis.service.impl.DatabaseImpl
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
@ -35,7 +35,8 @@ class Application extends Actor with StrictLogging {
case Success(_) => case Success(_) =>
val ariyala = context.system.actorOf(Ariyala.props, "ariyala") val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
val storage = context.system.actorOf(DatabaseImpl.props, "storage") val storage = context.system.actorOf(DatabaseImpl.props, "storage")
val http = new RootEndpoint(context.system, storage, ariyala) val party = context.system.actorOf(PartyService.props(storage), "party")
val http = new RootEndpoint(context.system, party, ariyala)
logger.info(s"start server at $host:$port") logger.info(s"start server at $host:$port")
val bind = Http()(context.system).bindAndHandle(http.route, host, port) val bind = Http()(context.system).bindAndHandle(http.route, host, port)

View File

@ -12,6 +12,7 @@ import akka.actor.ActorRef
import akka.pattern.ask import akka.pattern.ask
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.models.User import me.arcanis.ffxivbis.models.User
import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
@ -22,6 +23,9 @@ class UserHelper(storage: ActorRef) {
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Int] =
(storage ? DatabaseUserHandler.AddUser(user, isHashedPassword)).mapTo[Int] (storage ? DatabaseUserHandler.AddUser(user, isHashedPassword)).mapTo[Int]
def newPartyId(implicit executionContext: ExecutionContext, timeout: Timeout): Future[String] =
(storage ? PartyService.GetNewPartyId).mapTo[String]
def user(partyId: String, username: String) def user(partyId: String, username: String)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] = (implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]] (storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]]

View File

@ -29,7 +29,7 @@ import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout)
extends BiSHelper(storage, ariyala) with Authorization with JsonSupport with HttpHandler { extends BiSHelper(storage, ariyala) with Authorization with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS def route: Route = createBiS ~ getBiS ~ modifyBiS
@ -54,8 +54,6 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
) )
def createBiS: Route = def createBiS: Route =
path("party" / Segment / "bis") { partyId => path("party" / Segment / "bis") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
put { put {
@ -70,8 +68,6 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
} }
} }
} }
}
}
@GET @GET
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@ -96,8 +92,6 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
) )
def getBiS: Route = def getBiS: Route =
path("party" / Segment / "bis") { partyId => path("party" / Segment / "bis") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
@ -113,8 +107,6 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
} }
} }
}
}
@POST @POST
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@ -137,8 +129,6 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
) )
def modifyBiS: Route = def modifyBiS: Route =
path("party" / Segment / "bis") { partyId => path("party" / Segment / "bis") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
@ -150,8 +140,7 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
} }
} }
} }
}
}
} }
} }
} }

View File

@ -55,8 +55,6 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def getLoot: Route = def getLoot: Route =
path("party" / Segment / "loot") { partyId => path("party" / Segment / "loot") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
@ -71,8 +69,6 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}
@POST @POST
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@ -95,8 +91,6 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def modifyLoot: Route = def modifyLoot: Route =
path("party" / Segment / "loot") { partyId => path("party" / Segment / "loot") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post { post {
@ -111,8 +105,6 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}
@PUT @PUT
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@ -139,8 +131,6 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def suggestLoot: Route = def suggestLoot: Route =
path("party" / Segment / "loot") { partyId => path("party" / Segment / "loot") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
put { put {
@ -155,5 +145,3 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}

View File

@ -55,8 +55,6 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
) )
def getParty: Route = def getParty: Route =
path("party" / Segment) { partyId => path("party" / Segment) { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
get { get {
@ -71,8 +69,6 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
} }
} }
} }
}
}
@POST @POST
@Path("party/{partyId}") @Path("party/{partyId}")
@ -95,8 +91,6 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
) )
def modifyParty: Route = def modifyParty: Route =
path("party" / Segment) { partyId => path("party" / Segment) { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
entity(as[PlayerActionResponse]) { action => entity(as[PlayerActionResponse]) { action =>
@ -110,5 +104,3 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
} }
} }
} }
}
}

View File

@ -12,8 +12,11 @@ import akka.actor.ActorRef
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route import akka.http.scaladsl.server.Route
import akka.util.Timeout import akka.util.Timeout
import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport
class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout: Timeout) { class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)
(implicit timeout: Timeout)
extends JsonSupport with HttpHandler {
private val biSEndpoint = new BiSEndpoint(storage, ariyala) private val biSEndpoint = new BiSEndpoint(storage, ariyala)
private val lootEndpoint = new LootEndpoint(storage) private val lootEndpoint = new LootEndpoint(storage)
@ -21,5 +24,9 @@ class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout:
private val userEndpoint = new UserEndpoint(storage) private val userEndpoint = new UserEndpoint(storage)
def route: Route = def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
} }
}
}

View File

@ -28,21 +28,18 @@ import scala.util.{Failure, Success}
@Path("api/v1") @Path("api/v1")
class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout) class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) with Authorization with JsonSupport with HttpHandler { extends UserHelper(storage) with Authorization with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers
@PUT @PUT
@Path("party/{partyId}/create") @Path("party")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation(summary = "create new party", description = "Create new party with specified ID", @Operation(summary = "create new party", description = "Create new party with specified ID",
parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"),
),
requestBody = new RequestBody(description = "party administrator description", required = true, requestBody = new RequestBody(description = "party administrator description", required = true,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))), content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
responses = Array( responses = Array(
new ApiResponse(responseCode = "201", description = "Party has been created"), new ApiResponse(responseCode = "200", description = "Party has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"), new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists"), new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists"),
new ApiResponse(responseCode = "500", description = "Internal server error"), new ApiResponse(responseCode = "500", description = "Internal server error"),
@ -50,18 +47,18 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
tags = Array("party"), tags = Array("party"),
) )
def createParty: Route = def createParty: Route =
path("party" / Segment / "create") { partyId => path("party") {
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
put { put {
entity(as[UserResponse]) { user => entity(as[UserResponse]) { user =>
onComplete(newPartyId) {
case Success(partyId) =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
onComplete(addUser(admin, isHashedPassword = false)) { onComplete(addUser(admin, isHashedPassword = false)) {
case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty) case Success(_) => complete(PartyIdResponse(partyId))
case Failure(exception) => throw exception case Failure(exception) => throw exception
} }
} case Failure(exception) => throw exception
} }
} }
} }
@ -89,8 +86,6 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def createUser: Route = def createUser: Route =
path("party" / Segment / "users") { partyId => path("party" / Segment / "users") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
post { post {
@ -105,8 +100,6 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}
@DELETE @DELETE
@Path("party/{partyId}/users/{username}") @Path("party/{partyId}/users/{username}")
@ -126,8 +119,6 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def deleteUser: Route = def deleteUser: Route =
path("party" / Segment / "users" / Segment) { (partyId, username) => path("party" / Segment / "users" / Segment) { (partyId, username) =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
delete { delete {
@ -139,8 +130,6 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}
@GET @GET
@Path("party/{partyId}/users") @Path("party/{partyId}/users")
@ -163,8 +152,6 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
) )
def getUsers: Route = def getUsers: Route =
path("party" / Segment / "users") { partyId => path("party" / Segment / "users") { partyId =>
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
get { get {
@ -177,5 +164,3 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
} }
} }
} }
}
}

View File

@ -28,6 +28,7 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply)
implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply)
implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply) implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply)
implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)

View File

@ -0,0 +1,6 @@
package me.arcanis.ffxivbis.http.api.v1.json
import io.swagger.v3.oas.annotations.media.Schema
case class PartyIdResponse(
@Schema(description = "party id", required = true) partyId: String)

View File

@ -16,20 +16,26 @@ import akka.util.Timeout
import me.arcanis.ffxivbis.http.UserHelper import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.models.{Party, Permission, User} import me.arcanis.ffxivbis.models.{Party, Permission, User}
import scala.util.{Failure, Success}
class IndexView(storage: ActorRef)(implicit timeout: Timeout) class IndexView(storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) { extends UserHelper(storage) {
def route: Route = createParty ~ getIndex def route: Route = createParty ~ getIndex
def createParty: Route = def createParty: Route =
path("party" / Segment / "create") { partyId => path("party") {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
post { post {
formFields("username".as[String], "password".as[String]) { (username, password) => formFields("username".as[String], "password".as[String]) { (username, password) =>
onComplete(newPartyId) {
case Success(partyId) =>
val user = User(partyId, username, password, Permission.admin) val user = User(partyId, username, password, Permission.admin)
onComplete(addUser(user, isHashedPassword = false)) { onComplete(addUser(user, isHashedPassword = false)) {
case _ => redirect(s"/party/$partyId", StatusCodes.Found) case _ => redirect(s"/party/$partyId", StatusCodes.Found)
} }
case Failure(exception) => throw exception
}
} }
} }
} }
@ -40,7 +46,7 @@ class IndexView(storage: ActorRef)(implicit timeout: Timeout)
get { get {
parameters("partyId".as[String].?) { parameters("partyId".as[String].?) {
case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found) case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found)
case _ => complete((StatusCodes.OK, RootView.toHtml(IndexView.template))) case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template))
} }
} }
} }
@ -58,7 +64,7 @@ object IndexView {
), ),
body( body(
form(action:=s"party/${Party.randomPartyId}/create", method:="post")( form(action:=s"party", method:="post")(
label("create a new party"), label("create a new party"),
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"), input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"), input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),

View File

@ -22,7 +22,7 @@ import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import spray.json._ import spray.json._
import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try} import scala.util.Try
class Ariyala extends Actor with StrictLogging { class Ariyala extends Actor with StrictLogging {
import Ariyala._ import Ariyala._

View File

@ -37,3 +37,9 @@ trait Database extends Actor with StrictLogging {
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty) loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
} yield Party(partyId, context.system.settings.config, players, bis, loot) } yield Party(partyId, context.system.settings.config, players, bis, loot)
} }
object Database {
trait DatabaseRequest {
def partyId: String
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2019 Evgeniy Alekseev.
*
* This file is part of ffxivbis
* (see https://github.com/arcan1s/ffxivbis).
*
* License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
*/
package me.arcanis.ffxivbis.service
import akka.actor.{Actor, ActorRef, Props}
import akka.pattern.{ask, pipe}
import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.models.Party
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}
class PartyService(storage: ActorRef) extends Actor with StrictLogging {
import PartyService._
import me.arcanis.ffxivbis.utils.Implicits._
private val cacheTimeout: FiniteDuration =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.cache-timeout")
implicit private val executionContext: ExecutionContext = context.dispatcher
implicit private val timeout: Timeout =
context.system.settings.config.getDuration("me.arcanis.ffxivbis.settings.request-timeout")
override def receive: Receive = handle(Map.empty)
private def handle(cache: Map[String, Party]): Receive = {
case ForgetParty(partyId) =>
context become handle(cache - partyId)
case GetNewPartyId =>
val client = sender()
getPartyId.pipeTo(client)
case req @ impl.DatabasePartyHandler.GetParty(partyId) =>
val client = sender()
val party = cache.get(partyId) match {
case Some(party) => Future.successful(party)
case None =>
(storage ? req).mapTo[Party].map { party =>
context become handle(cache + (partyId -> party))
context.system.scheduler.scheduleOnce(cacheTimeout, self, ForgetParty(partyId))
party
}
}
party.pipeTo(client)
case req: Database.DatabaseRequest =>
self ! ForgetParty(req.partyId)
storage.forward(req)
}
private def getPartyId: Future[String] = {
val partyId = Party.randomPartyId
(storage ? impl.DatabaseUserHandler.Exists(partyId)).mapTo[Boolean].flatMap {
case true => getPartyId
case false => Future.successful(partyId)
}
}
}
object PartyService {
def props(storage: ActorRef): Props = Props(new PartyService(storage))
case class ForgetParty(partyId: String)
case object GetNewPartyId
}

View File

@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.impl package me.arcanis.ffxivbis.service.impl
import akka.pattern.pipe import akka.pattern.pipe
import me.arcanis.ffxivbis.models.{BiS, Piece, PlayerId} import me.arcanis.ffxivbis.models.{Piece, PlayerId}
import me.arcanis.ffxivbis.service.Database import me.arcanis.ffxivbis.service.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>
@ -33,7 +33,11 @@ trait DatabaseBiSHandler { this: Database =>
} }
object DatabaseBiSHandler { object DatabaseBiSHandler {
case class AddPieceToBis(playerId: PlayerId, piece: Piece) case class AddPieceToBis(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
case class GetBiS(partyId: String, playerId: Option[PlayerId]) override def partyId: String = playerId.partyId
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece) }
case class GetBiS(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
} }

View File

@ -37,8 +37,12 @@ trait DatabaseLootHandler { this: Database =>
} }
object DatabaseLootHandler { object DatabaseLootHandler {
case class AddPieceTo(playerId: PlayerId, piece: Piece) case class AddPieceTo(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
case class GetLoot(partyId: String, playerId: Option[PlayerId]) override def partyId: String = playerId.partyId
case class RemovePieceFrom(playerId: PlayerId, piece: Piece) }
case class SuggestLoot(partyId: String, piece: Piece) case class GetLoot(partyId: String, playerId: Option[PlayerId]) extends Database.DatabaseRequest
case class RemovePieceFrom(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class SuggestLoot(partyId: String, piece: Piece) extends Database.DatabaseRequest
} }

View File

@ -47,8 +47,14 @@ trait DatabasePartyHandler { this: Database =>
} }
object DatabasePartyHandler { object DatabasePartyHandler {
case class AddPlayer(player: Player) case class AddPlayer(player: Player) extends Database.DatabaseRequest {
case class GetParty(partyId: String) override def partyId: String = player.partyId
case class GetPlayer(playerId: PlayerId) }
case class RemovePlayer(playerId: PlayerId) case class GetParty(partyId: String) extends Database.DatabaseRequest
case class GetPlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
case class RemovePlayer(playerId: PlayerId) extends Database.DatabaseRequest {
override def partyId: String = playerId.partyId
}
} }

View File

@ -25,6 +25,10 @@ trait DatabaseUserHandler { this: Database =>
val client = sender() val client = sender()
profile.deleteUser(partyId, username).pipeTo(client) profile.deleteUser(partyId, username).pipeTo(client)
case Exists(partyId) =>
val client = sender()
profile.exists(partyId).pipeTo(client)
case GetUser(partyId, username) => case GetUser(partyId, username) =>
val client = sender() val client = sender()
profile.getUser(partyId, username).pipeTo(client) profile.getUser(partyId, username).pipeTo(client)
@ -36,8 +40,11 @@ trait DatabaseUserHandler { this: Database =>
} }
object DatabaseUserHandler { object DatabaseUserHandler {
case class AddUser(user: User, isHashedPassword: Boolean) case class AddUser(user: User, isHashedPassword: Boolean) extends Database.DatabaseRequest {
case class DeleteUser(partyId: String, username: String) override def partyId: String = user.partyId
case class GetUser(partyId: String, username: String) }
case class GetUsers(partyId: String) case class DeleteUser(partyId: String, username: String) extends Database.DatabaseRequest
case class Exists(partyId: String) extends Database.DatabaseRequest
case class GetUser(partyId: String, username: String) extends Database.DatabaseRequest
case class GetUsers(partyId: String) extends Database.DatabaseRequest
} }

View File

@ -42,6 +42,8 @@ trait UsersProfile { this: DatabaseProfile =>
def deleteUser(partyId: String, username: String): Future[Int] = def deleteUser(partyId: String, username: String): Future[Int] =
db.run(user(partyId, Some(username)).delete) db.run(user(partyId, Some(username)).delete)
def exists(partyId: String): Future[Boolean] =
db.run(user(partyId, None).exists.result)
def getUser(partyId: String, username: String): Future[Option[User]] = def getUser(partyId: String, username: String): Future[Option[User]] =
db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser)) db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser))
def getUsers(partyId: String): Future[Seq[User]] = def getUsers(partyId: String): Future[Seq[User]] =

View File

@ -17,11 +17,14 @@ import scala.concurrent.duration.FiniteDuration
import scala.language.implicitConversions import scala.language.implicitConversions
object Implicits { object Implicits {
implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes match { implicit def getBooleanFromOptionString(maybeYes: Option[String]): Boolean = maybeYes.map(_.toLowerCase) match {
case Some("yes" | "on") => true case Some("yes" | "on") => true
case _ => false case _ => false
} }
implicit def getFiniteDuration(duration: Duration): Timeout = implicit def getFiniteDuration(duration: Duration): FiniteDuration =
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
implicit def getTimeout(duration: Duration): Timeout =
FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS) FiniteDuration(duration.toNanos, TimeUnit.NANOSECONDS)
} }

View File

@ -11,7 +11,7 @@ import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.BiS import me.arcanis.ffxivbis.models.BiS
import me.arcanis.ffxivbis.service.{Ariyala, impl} import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
@ -31,7 +31,8 @@ class BiSEndpointTest extends WordSpec
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props) private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props) private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val route: Route = new BiSEndpoint(storage, ariyala)(timeout).route private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new BiSEndpoint(party, ariyala)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase override def testConfig: Config = Settings.withRandomDatabase

View File

@ -10,7 +10,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.impl import me.arcanis.ffxivbis.service.{PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
@ -29,7 +29,8 @@ class LootEndpointTest extends WordSpec
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout) implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props) private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val route: Route = new LootEndpoint(storage)(timeout).route private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new LootEndpoint(party)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase override def testConfig: Config = Settings.withRandomDatabase

View File

@ -10,7 +10,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.{Ariyala, impl} import me.arcanis.ffxivbis.service.{Ariyala, PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
@ -30,7 +30,8 @@ class PartyEndpointTest extends WordSpec
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props) private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val ariyala: ActorRef = system.actorOf(Ariyala.props) private val ariyala: ActorRef = system.actorOf(Ariyala.props)
private val route: Route = new PlayerEndpoint(storage, ariyala)(timeout).route private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new PlayerEndpoint(party, ariyala)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase override def testConfig: Config = Settings.withRandomDatabase

View File

@ -9,7 +9,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.service.impl import me.arcanis.ffxivbis.service.{PartyService, impl}
import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.storage.Migration
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
@ -22,12 +22,14 @@ class UserEndpointTest extends WordSpec
private val auth: Authorization = private val auth: Authorization =
Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword))
private val endpoint: Uri = Uri(s"/party/${Fixtures.partyId}/users") private def endpoint: Uri = Uri(s"/party/$partyId/users")
private val timeout: FiniteDuration = 60 seconds private val timeout: FiniteDuration = 60 seconds
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout) implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
private var partyId: String = Fixtures.partyId
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props) private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.props)
private val route: Route = new UserEndpoint(storage)(timeout).route private val party: ActorRef = system.actorOf(PartyService.props(storage))
private val route: Route = new UserEndpoint(party)(timeout).route
override def testConfig: Config = Settings.withRandomDatabase override def testConfig: Config = Settings.withRandomDatabase
@ -43,18 +45,17 @@ class UserEndpointTest extends WordSpec
"api v1 users endpoint" must { "api v1 users endpoint" must {
"create a party" in { "create a party" in {
val uri = Uri(s"/party/${Fixtures.partyId}/create") val uri = Uri(s"/party")
val entity = UserResponse.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) val entity = UserResponse.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword)
println(entity)
Put(uri, entity) ~> route ~> check { Put(uri, entity) ~> route ~> check {
status shouldEqual StatusCodes.Created status shouldEqual StatusCodes.OK
responseAs[String] shouldEqual "" partyId = responseAs[PartyIdResponse].partyId
} }
} }
"add user" in { "add user" in {
val entity = UserResponse.fromUser(Fixtures.userGet).copy(password = Fixtures.userPassword2) val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2)
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted
@ -70,13 +71,28 @@ class UserEndpointTest extends WordSpec
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
val users = responseAs[Seq[UserResponse]] val users = responseAs[Seq[UserResponse]]
users.map(_.partyId).distinct shouldEqual Seq(Fixtures.partyId) users.map(_.partyId).distinct shouldEqual Seq(partyId)
users.map(user => user.username -> user.permission).toMap shouldEqual party users.map(user => user.username -> user.permission).toMap shouldEqual party
} }
} }
"remove user" in { "remove user" in {
val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId)
Delete(endpoint.toString + s"/${entity.username}").withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted
}
val party = Seq(Fixtures.userAdmin)
.map(user => user.username -> Some(user.permission)).toMap
Get(endpoint).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.OK
val users = responseAs[Seq[UserResponse]]
users.map(_.partyId).distinct shouldEqual Seq(partyId)
users.map(user => user.username -> user.permission).toMap shouldEqual party
}
} }
} }