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">
<appender-ref ref="http" />
</logger>
<logger name="slick.jdbc.JdbcBackend.statement" level="DEBUG" />
<logger name="slick" level="INFO" />
</configuration>

View File

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

View File

@ -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)

View File

@ -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]]

View File

@ -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
}
}
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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)

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,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"),

View File

@ -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._

View File

@ -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
}
}

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
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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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]] =

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
}
}