diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 2cfefec..402138e 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -11,6 +11,6 @@
-
+
diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf
index 378cf84..34e2efd 100644
--- a/src/main/resources/reference.conf
+++ b/src/main/resources/reference.conf
@@ -42,6 +42,8 @@ me.arcanis.ffxivbis {
]
# general request timeout, duratin, required
request-timeout = 10s
+ # party in-memory storage lifetime
+ cache-timeout = 1m
}
web {
diff --git a/src/main/scala/me/arcanis/ffxivbis/Application.scala b/src/main/scala/me/arcanis/ffxivbis/Application.scala
index 86ce194..98588ea 100644
--- a/src/main/scala/me/arcanis/ffxivbis/Application.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/Application.scala
@@ -13,7 +13,7 @@ import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import com.typesafe.scalalogging.StrictLogging
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.storage.Migration
@@ -35,7 +35,8 @@ class Application extends Actor with StrictLogging {
case Success(_) =>
val ariyala = context.system.actorOf(Ariyala.props, "ariyala")
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")
val bind = Http()(context.system).bindAndHandle(http.route, host, port)
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
index b426499..d2e38af 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala
@@ -12,6 +12,7 @@ import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import me.arcanis.ffxivbis.models.User
+import me.arcanis.ffxivbis.service.PartyService
import me.arcanis.ffxivbis.service.impl.DatabaseUserHandler
import scala.concurrent.{ExecutionContext, Future}
@@ -22,6 +23,9 @@ class UserHelper(storage: ActorRef) {
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[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)
(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Option[User]] =
(storage ? DatabaseUserHandler.GetUser(partyId, username)).mapTo[Option[User]]
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
index 13dbc12..214df6f 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala
@@ -29,7 +29,7 @@ import scala.util.{Failure, Success}
@Path("api/v1")
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
@@ -54,18 +54,14 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
)
def createBiS: Route =
path("party" / Segment / "bis") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
- put {
- entity(as[PlayerBiSLinkResponse]) { bisLink =>
- val playerId = bisLink.playerId.withPartyId(partyId)
- onComplete(putBiS(playerId, bisLink.link)) {
- case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
+ put {
+ entity(as[PlayerBiSLinkResponse]) { bisLink =>
+ val playerId = bisLink.playerId.withPartyId(partyId)
+ onComplete(putBiS(playerId, bisLink.link)) {
+ case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
@@ -96,23 +92,19 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
)
def getBiS: Route =
path("party" / Segment / "bis") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
- get {
- parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
- val playerId = PlayerId(partyId, maybeNick, maybeJob)
- onComplete(bis(partyId, playerId)) {
- case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
+ get {
+ parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
+ val playerId = PlayerId(partyId, maybeNick, maybeJob)
+ onComplete(bis(partyId, playerId)) {
+ case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
+ case Failure(exception) => throw exception
}
}
-
}
}
+
}
}
@@ -137,21 +129,18 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
)
def modifyBiS: Route =
path("party" / Segment / "bis") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
- post {
- entity(as[PieceActionResponse]) { action =>
- val playerId = action.playerIdResponse.withPartyId(partyId)
- onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
- case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
+ post {
+ entity(as[PieceActionResponse]) { action =>
+ val playerId = action.playerIdResponse.withPartyId(partyId)
+ onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
+ case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
+
}
}
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala
index 35a39b2..1f7db4c 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala
@@ -55,18 +55,14 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def getLoot: Route =
path("party" / Segment / "loot") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
- get {
- parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
- val playerId = PlayerId(partyId, maybeNick, maybeJob)
- onComplete(loot(partyId, playerId)) {
- case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
+ get {
+ parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
+ val playerId = PlayerId(partyId, maybeNick, maybeJob)
+ onComplete(loot(partyId, playerId)) {
+ case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
+ case Failure(exception) => throw exception
}
}
}
@@ -95,18 +91,14 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def modifyLoot: Route =
path("party" / Segment / "loot") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
- post {
- entity(as[PieceActionResponse]) { action =>
- val playerId = action.playerIdResponse.withPartyId(partyId)
- onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
- case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
+ post {
+ entity(as[PieceActionResponse]) { action =>
+ val playerId = action.playerIdResponse.withPartyId(partyId)
+ onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
+ case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
@@ -139,17 +131,13 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def suggestLoot: Route =
path("party" / Segment / "loot") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
- put {
- entity(as[PieceResponse]) { piece =>
- onComplete(suggestPiece(partyId, piece.toPiece)) {
- case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId))
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
+ put {
+ entity(as[PieceResponse]) { piece =>
+ onComplete(suggestPiece(partyId, piece.toPiece)) {
+ case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId))
+ case Failure(exception) => throw exception
}
}
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala
index fb6e7c3..75bfeb3 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala
@@ -55,18 +55,14 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
)
def getParty: Route =
path("party" / Segment) { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
- get {
- parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
- val playerId = PlayerId(partyId, maybeNick, maybeJob)
- onComplete(getPlayers(partyId, playerId)) {
- case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ =>
+ get {
+ parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) =>
+ val playerId = PlayerId(partyId, maybeNick, maybeJob)
+ onComplete(getPlayers(partyId, playerId)) {
+ case Success(response) => complete(response.map(PlayerResponse.fromPlayer))
+ case Failure(exception) => throw exception
}
}
}
@@ -95,17 +91,13 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
)
def modifyParty: Route =
path("party" / Segment) { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
- entity(as[PlayerActionResponse]) { action =>
- val player = action.playerIdResponse.toPlayer.copy(partyId = partyId)
- onComplete(doModifyPlayer(action.action, player)) {
- case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
+ entity(as[PlayerActionResponse]) { action =>
+ val player = action.playerIdResponse.toPlayer.copy(partyId = partyId)
+ onComplete(doModifyPlayer(action.action, player)) {
+ case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala
index dec7172..8d3ebfd 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala
@@ -12,8 +12,11 @@ import akka.actor.ActorRef
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
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 lootEndpoint = new LootEndpoint(storage)
@@ -21,5 +24,9 @@ class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)(implicit timeout:
private val userEndpoint = new UserEndpoint(storage)
def route: Route =
- biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
+ handleExceptions(exceptionHandler) {
+ handleRejections(rejectionHandler) {
+ biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
+ }
+ }
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala
index f7d51d3..da7104b 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala
@@ -28,21 +28,18 @@ import scala.util.{Failure, Success}
@Path("api/v1")
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
@PUT
- @Path("party/{partyId}/create")
+ @Path("party")
@Consumes(value = Array("application/json"))
@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,
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
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 = "406", description = "Party with the specified ID already exists"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
@@ -50,18 +47,18 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
tags = Array("party"),
)
def createParty: Route =
- path("party" / Segment / "create") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- put {
- entity(as[UserResponse]) { user =>
+ path("party") {
+ extractExecutionContext { implicit executionContext =>
+ put {
+ entity(as[UserResponse]) { user =>
+ onComplete(newPartyId) {
+ case Success(partyId) =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
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
}
}
}
@@ -89,18 +86,14 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def createUser: Route =
path("party" / Segment / "users") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
- post {
- entity(as[UserResponse]) { user =>
- val withPartyId = user.toUser.copy(partyId = partyId)
- onComplete(addUser(withPartyId, isHashedPassword = false)) {
- case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
+ post {
+ entity(as[UserResponse]) { user =>
+ val withPartyId = user.toUser.copy(partyId = partyId)
+ onComplete(addUser(withPartyId, isHashedPassword = false)) {
+ case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
@@ -126,16 +119,12 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def deleteUser: Route =
path("party" / Segment / "users" / Segment) { (partyId, username) =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
- delete {
- onComplete(removeUser(partyId, username)) {
- case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
+ delete {
+ onComplete(removeUser(partyId, username)) {
+ case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
+ case Failure(exception) => throw exception
}
}
}
@@ -163,16 +152,12 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
)
def getUsers: Route =
path("party" / Segment / "users") { partyId =>
- handleExceptions(exceptionHandler) {
- handleRejections(rejectionHandler) {
- extractExecutionContext { implicit executionContext =>
- authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
- get {
- onComplete(users(partyId)) {
- case Success(response) => complete(response.map(UserResponse.fromUser))
- case Failure(exception) => throw exception
- }
- }
+ extractExecutionContext { implicit executionContext =>
+ authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ =>
+ get {
+ onComplete(users(partyId)) {
+ case Success(response) => complete(response.map(UserResponse.fromUser))
+ case Failure(exception) => throw exception
}
}
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala
index a1b4b09..81df641 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala
@@ -28,6 +28,7 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission)
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 playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply)
implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply)
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala
new file mode 100644
index 0000000..a650350
--- /dev/null
+++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala
@@ -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)
diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala
index 1c52c2f..c4a9b84 100644
--- a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala
@@ -16,19 +16,25 @@ import akka.util.Timeout
import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.models.{Party, Permission, User}
+import scala.util.{Failure, Success}
+
class IndexView(storage: ActorRef)(implicit timeout: Timeout)
extends UserHelper(storage) {
def route: Route = createParty ~ getIndex
def createParty: Route =
- path("party" / Segment / "create") { partyId =>
+ path("party") {
extractExecutionContext { implicit executionContext =>
post {
formFields("username".as[String], "password".as[String]) { (username, password) =>
- val user = User(partyId, username, password, Permission.admin)
- onComplete(addUser(user, isHashedPassword = false)) {
- case _ => redirect(s"/party/$partyId", StatusCodes.Found)
+ onComplete(newPartyId) {
+ case Success(partyId) =>
+ val user = User(partyId, username, password, Permission.admin)
+ onComplete(addUser(user, isHashedPassword = false)) {
+ case _ => redirect(s"/party/$partyId", StatusCodes.Found)
+ }
+ case Failure(exception) => throw exception
}
}
}
@@ -40,7 +46,7 @@ class IndexView(storage: ActorRef)(implicit timeout: Timeout)
get {
parameters("partyId".as[String].?) {
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(
- form(action:=s"party/${Party.randomPartyId}/create", method:="post")(
+ form(action:=s"party", method:="post")(
label("create a new party"),
input(name:="username", id:="username", placeholder:="username", title:="username", `type`:="text"),
input(name:="password", id:="password", placeholder:="password", title:="password", `type`:="password"),
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala b/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala
index cac3c09..a1d3e59 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/Ariyala.scala
@@ -22,7 +22,7 @@ import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import spray.json._
import scala.concurrent.{ExecutionContext, Future}
-import scala.util.{Failure, Success, Try}
+import scala.util.Try
class Ariyala extends Actor with StrictLogging {
import Ariyala._
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/Database.scala b/src/main/scala/me/arcanis/ffxivbis/service/Database.scala
index faa0db2..b7dba24 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/Database.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/Database.scala
@@ -37,3 +37,9 @@ trait Database extends Actor with StrictLogging {
loot <- if (withLoot) profile.getPieces(partyId) else Future(Seq.empty)
} yield Party(partyId, context.system.settings.config, players, bis, loot)
}
+
+object Database {
+ trait DatabaseRequest {
+ def partyId: String
+ }
+}
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala
new file mode 100644
index 0000000..d3bc22c
--- /dev/null
+++ b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala
@@ -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
+}
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala
index 6b29033..b9d2eb6 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseBiSHandler.scala
@@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.service.impl
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
trait DatabaseBiSHandler { this: Database =>
@@ -33,7 +33,11 @@ trait DatabaseBiSHandler { this: Database =>
}
object DatabaseBiSHandler {
- case class AddPieceToBis(playerId: PlayerId, piece: Piece)
- case class GetBiS(partyId: String, playerId: Option[PlayerId])
- case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece)
+ case class AddPieceToBis(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
+ override def partyId: String = playerId.partyId
+ }
+ 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
+ }
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseLootHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseLootHandler.scala
index 39f179e..ae10f24 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseLootHandler.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseLootHandler.scala
@@ -37,8 +37,12 @@ trait DatabaseLootHandler { this: Database =>
}
object DatabaseLootHandler {
- case class AddPieceTo(playerId: PlayerId, piece: Piece)
- case class GetLoot(partyId: String, playerId: Option[PlayerId])
- case class RemovePieceFrom(playerId: PlayerId, piece: Piece)
- case class SuggestLoot(partyId: String, piece: Piece)
+ case class AddPieceTo(playerId: PlayerId, piece: Piece) extends Database.DatabaseRequest {
+ override def partyId: String = playerId.partyId
+ }
+ 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
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala
index 3b86b2a..ed05b35 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabasePartyHandler.scala
@@ -47,8 +47,14 @@ trait DatabasePartyHandler { this: Database =>
}
object DatabasePartyHandler {
- case class AddPlayer(player: Player)
- case class GetParty(partyId: String)
- case class GetPlayer(playerId: PlayerId)
- case class RemovePlayer(playerId: PlayerId)
+ case class AddPlayer(player: Player) extends Database.DatabaseRequest {
+ override def partyId: String = player.partyId
+ }
+ 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
+ }
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseUserHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseUserHandler.scala
index 8298120..ddb62b0 100644
--- a/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseUserHandler.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/service/impl/DatabaseUserHandler.scala
@@ -25,6 +25,10 @@ trait DatabaseUserHandler { this: Database =>
val client = sender()
profile.deleteUser(partyId, username).pipeTo(client)
+ case Exists(partyId) =>
+ val client = sender()
+ profile.exists(partyId).pipeTo(client)
+
case GetUser(partyId, username) =>
val client = sender()
profile.getUser(partyId, username).pipeTo(client)
@@ -36,8 +40,11 @@ trait DatabaseUserHandler { this: Database =>
}
object DatabaseUserHandler {
- case class AddUser(user: User, isHashedPassword: Boolean)
- case class DeleteUser(partyId: String, username: String)
- case class GetUser(partyId: String, username: String)
- case class GetUsers(partyId: String)
+ case class AddUser(user: User, isHashedPassword: Boolean) extends Database.DatabaseRequest {
+ override def partyId: String = user.partyId
+ }
+ 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
}
diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala
index 8680d48..2f8ef43 100644
--- a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala
@@ -42,6 +42,8 @@ trait UsersProfile { this: DatabaseProfile =>
def deleteUser(partyId: String, username: String): Future[Int] =
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]] =
db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser))
def getUsers(partyId: String): Future[Seq[User]] =
diff --git a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala
index ce924ae..ec43870 100644
--- a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala
+++ b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala
@@ -17,11 +17,14 @@ import scala.concurrent.duration.FiniteDuration
import scala.language.implicitConversions
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 _ => 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)
}
diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala
index 3691d38..a3cddfe 100644
--- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala
+++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala
@@ -11,7 +11,7 @@ import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
import me.arcanis.ffxivbis.http.api.v1.json._
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 org.scalatest.{Matchers, WordSpec}
@@ -31,7 +31,8 @@ class BiSEndpointTest extends WordSpec
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.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
diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala
index 7f6bc22..1ed7042 100644
--- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala
+++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala
@@ -10,7 +10,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
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 org.scalatest.{Matchers, WordSpec}
@@ -29,7 +29,8 @@ class LootEndpointTest extends WordSpec
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
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
diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala
index 5b69c89..c17d3af 100644
--- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala
+++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala
@@ -10,7 +10,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
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 org.scalatest.{Matchers, WordSpec}
@@ -30,7 +30,8 @@ class PartyEndpointTest extends WordSpec
private val storage: ActorRef = system.actorOf(impl.DatabaseImpl.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
diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala
index 609ef74..3b99653 100644
--- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala
+++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala
@@ -9,7 +9,7 @@ import akka.testkit.TestKit
import com.typesafe.config.Config
import me.arcanis.ffxivbis.{Fixtures, Settings}
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 org.scalatest.{Matchers, WordSpec}
@@ -22,12 +22,14 @@ class UserEndpointTest extends WordSpec
private val auth: Authorization =
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
implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(timeout)
+ private var partyId: String = Fixtures.partyId
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
@@ -43,18 +45,17 @@ class UserEndpointTest extends WordSpec
"api v1 users endpoint" must {
"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)
- println(entity)
Put(uri, entity) ~> route ~> check {
- status shouldEqual StatusCodes.Created
- responseAs[String] shouldEqual ""
+ status shouldEqual StatusCodes.OK
+ partyId = responseAs[PartyIdResponse].partyId
}
}
"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 {
status shouldEqual StatusCodes.Accepted
@@ -70,13 +71,28 @@ class UserEndpointTest extends WordSpec
status shouldEqual StatusCodes.OK
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
}
}
"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
+ }
}
}