8 Commits
0.9.1 ... 0.9.4

Author SHA1 Message Date
ad144534a9 0.9.4
* types api
* dist instead of assembly (assembly does not allow to use swagger easily)
* small swagger improvements
2019-11-15 01:27:15 +03:00
4700768aed report errors 2019-11-11 10:03:07 +03:00
557038c262 rename property 2019-11-05 00:28:21 +03:00
6e8b64feef some improvements in loot system 2019-11-04 15:19:18 +03:00
0a71a98482 postgres config update 2019-11-03 15:48:05 +03:00
69d35c95d9 fix migration 2019-11-03 14:14:48 +03:00
155790465e add notes about downloading jar 2019-11-02 15:29:48 +03:00
da00a60332 fix assembly build 2019-11-02 15:26:29 +03:00
40 changed files with 354 additions and 124 deletions

View File

@ -4,20 +4,26 @@ Service which allows to manage savage loot distribution easy.
## Installation and usage
In general installation process looks like:
In general compliation process looks like:
```bash
sbt assembly
sbt dist
```
Service can be run by using command:
Or alternatively you can download latest distribution zip from the releases page. Service can be run by using command:
```bash
java -cp ./target/scala-2.13/ffxivbis-scala-assembly-0.1.jar me.arcanis.ffxivbis.ffxivbis
bin/ffxivbis
```
from the extracted archive root.
## Web service
REST API documentation is available at `http://0.0.0.0:8000/swagger`. HTML representation is available at `http://0.0.0.0:8000`.
*Note*: host and port depend on configuration settings.
## Public service
There is also public service which is available at https://ffxivbis.arcanis.me.

View File

@ -4,6 +4,8 @@ scalaVersion := "2.13.1"
scalacOptions ++= Seq("-deprecation", "-feature")
enablePlugins(JavaAppPackaging)
assemblyMergeStrategy in assembly := {
case PathList("META-INF", xs @ _*) => MergeStrategy.discard
case "application.conf" => MergeStrategy.concat

View File

@ -1 +0,0 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")

View File

@ -1 +0,0 @@
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

3
project/plugins.sbt Normal file
View File

@ -0,0 +1,3 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.4")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1")

View File

@ -1,6 +1,6 @@
create table players (
party_id text not null,
player_id bigserial,
player_id bigserial unique,
created bigint not null,
nick text not null,
job text not null,
@ -9,7 +9,7 @@ create table players (
create unique index players_nick_job_idx on players(party_id, nick, job);
create table loot (
loot_id bigserial,
loot_id bigserial unique,
player_id bigint not null,
created bigint not null,
piece text not null,
@ -29,7 +29,7 @@ create unique index bis_piece_player_id_idx on bis(player_id, piece);
create table users (
party_id text not null,
user_id bigserial,
user_id bigserial unique,
username text not null,
password text not null,
permission text not null);

View File

@ -0,0 +1,5 @@
update loot set piece = 'left ring' where piece = 'leftRing';
update loot set piece = 'right ring' where piece = 'rightRing';
update bis set piece = 'left ring' where piece = 'leftRing';
update bis set piece = 'right ring' where piece = 'rightRing';

View File

@ -0,0 +1,5 @@
update loot set piece = 'left ring' where piece = 'leftRing';
update loot set piece = 'right ring' where piece = 'rightRing';
update bis set piece = 'left ring' where piece = 'leftRing';
update bis set piece = 'right ring' where piece = 'rightRing';

View File

@ -27,8 +27,11 @@ me.arcanis.ffxivbis {
profile = "slick.jdbc.PostgresProfile$"
db {
url = "jdbc:postgresql://localhost/ffxivbis"
user = "user"
password = "password"
user = "ffxivbis"
password = "ffxivbis"
connectionPool = disabled
keepAliveConnection = yes
}
numThreads = 10
}
@ -48,7 +51,7 @@ me.arcanis.ffxivbis {
web {
# address to bind, string, required
host = "0.0.0.0"
host = "127.0.0.1"
# port to bind, int, required
port = 8000
}

View File

@ -0,0 +1,24 @@
REST json API description to interact with FFXIVBiS service.
# Basic workflow
* Create party using `PUT /api/v1/party` endpoint. It consumes username and password of administrator (which can't be restored). As the result it returns unique id of created party.
* Create new users which have access to this party. Note that user is belong to specific party id and in scope of the specified party it must be unique.
* Add players with their best in slot sets (probably by using ariyala links).
* Add loot items if any.
* By using `PUT /api/v1/party/{partyId}/loot` API find players which are better for the specified loot.
* Add new loot item to the selected player.
# Limitations
# Authentication
For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`.
* `admin` permission means that the user is allowed to do anything, especially this permission is required to be able to add or modify users.
* `post` permission is required to deal with the most POST API endpoints, but to be precise only endpoints which modifies party content require this permission.
* `get` permission is required to have access to party.
`admin` permission includes any other permissions, `post` allows to perform get requests.
<security-definitions />

View File

@ -13,12 +13,12 @@ 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, PartyService}
import me.arcanis.ffxivbis.service.impl.DatabaseImpl
import me.arcanis.ffxivbis.service.{Ariyala, PartyService}
import me.arcanis.ffxivbis.storage.Migration
import scala.concurrent.{Await, ExecutionContext}
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext}
import scala.util.{Failure, Success}
class Application extends Actor with StrictLogging {

View File

@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.http
import akka.actor.ActorRef
import akka.pattern.ask
import akka.util.Timeout
import me.arcanis.ffxivbis.models.{BiS, Job, Piece}
import me.arcanis.ffxivbis.models.{BiS, Job}
import me.arcanis.ffxivbis.service.Ariyala
import scala.concurrent.{ExecutionContext, Future}

View File

@ -11,8 +11,8 @@ package me.arcanis.ffxivbis.http
import akka.actor.ActorRef
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.AuthenticationFailedRejection._
import akka.http.scaladsl.server._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.pattern.ask
import akka.util.Timeout
import me.arcanis.ffxivbis.models.{Permission, User}

View File

@ -9,16 +9,24 @@
package me.arcanis.ffxivbis.http
import com.github.swagger.akka.SwaggerHttpService
import com.github.swagger.akka.model.Info
import com.github.swagger.akka.model.{Info, License}
import io.swagger.v3.oas.models.security.SecurityScheme
import scala.io.Source
object Swagger extends SwaggerHttpService {
override val apiClasses: Set[Class[_]] = Set(
classOf[api.v1.BiSEndpoint], classOf[api.v1.LootEndpoint],
classOf[api.v1.PlayerEndpoint], classOf[api.v1.UserEndpoint]
classOf[api.v1.PlayerEndpoint], classOf[api.v1.TypesEndpoint],
classOf[api.v1.UserEndpoint]
)
override val info: Info = Info()
override val info: Info = Info(
description = Source.fromResource("swagger-info/description.md").mkString,
version = getClass.getPackage.getImplementationVersion,
title = "FFXIV static loot tracker",
license = Some(License("BSD", "https://raw.githubusercontent.com/arcan1s/ffxivbis/master/LICENSE"))
)
private val basicAuth = new SecurityScheme()
.description("basic http auth")

View File

@ -13,16 +13,15 @@ import akka.http.scaladsl.model.{HttpEntity, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import com.typesafe.scalalogging.StrictLogging
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, BiSHelper}
import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@ -44,10 +43,14 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse])))),
responses = Array(
new ApiResponse(responseCode = "201", description = "Best in slot set has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"),
@ -83,9 +86,12 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("best in slot"),
@ -119,10 +125,14 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
responses = Array(
new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("best in slot"),
@ -133,7 +143,7 @@ class BiSEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit ti
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
entity(as[PieceActionResponse]) { action =>
val playerId = action.playerIdResponse.withPartyId(partyId)
val playerId = action.playerId.withPartyId(partyId)
onComplete(doModifyBiS(action.action, playerId, action.piece.toPiece)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception

View File

@ -15,13 +15,13 @@ import akka.http.scaladsl.server._
import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, PUT, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, LootHelper}
import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@ -46,9 +46,12 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse]))
))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"),
@ -81,10 +84,14 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse])))),
responses = Array(
new ApiResponse(responseCode = "202", description = "Loot list has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("loot"),
@ -95,7 +102,7 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
post {
entity(as[PieceActionResponse]) { action =>
val playerId = action.playerIdResponse.withPartyId(partyId)
val playerId = action.playerId.withPartyId(partyId)
onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception
@ -121,10 +128,14 @@ class LootEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])),
))),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("loot"),

View File

@ -15,13 +15,13 @@ import akka.http.scaladsl.server._
import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, GET, POST, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.models.PlayerId
import scala.util.{Failure, Success}
@ -46,9 +46,12 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])),
))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))),
tags = Array("party"),
@ -81,10 +84,14 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse])))),
responses = Array(
new ApiResponse(responseCode = "202", description = "Party has been modified"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))),
tags = Array("party"),
@ -94,7 +101,7 @@ class PlayerEndpoint(override val storage: ActorRef, ariyala: ActorRef)(implicit
extractExecutionContext { implicit executionContext =>
authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ =>
entity(as[PlayerActionResponse]) { action =>
val player = action.playerIdResponse.toPlayer.copy(partyId = partyId)
val player = action.playerId.toPlayer.copy(partyId = partyId)
onComplete(doModifyPlayer(action.action, player)) {
case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty)
case Failure(exception) => throw exception

View File

@ -21,12 +21,14 @@ class RootApiV1Endpoint(storage: ActorRef, ariyala: ActorRef)
private val biSEndpoint = new BiSEndpoint(storage, ariyala)
private val lootEndpoint = new LootEndpoint(storage)
private val playerEndpoint = new PlayerEndpoint(storage, ariyala)
private val typesEndpoint = new TypesEndpoint
private val userEndpoint = new UserEndpoint(storage)
def route: Route =
handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~ userEndpoint.route
biSEndpoint.route ~ lootEndpoint.route ~ playerEndpoint.route ~
typesEndpoint.route ~ userEndpoint.route
}
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.http.api.v1
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Permission, Piece}
@Path("api/v1")
class TypesEndpoint extends JsonSupport {
def route: Route = getJobs ~ getPermissions ~ getPieces
@GET
@Path("types/jobs")
@Produces(value = Array("application/json"))
@Operation(summary = "jobs list", description = "Returns the available jobs",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available jobs",
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("types"),
)
def getJobs: Route =
path("types" / "jobs") {
get {
complete(Job.available.map(_.toString))
}
}
@GET
@Path("types/permissions")
@Produces(value = Array("application/json"))
@Operation(summary = "permissions list", description = "Returns the available permissions",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available permissions",
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("types"),
)
def getPermissions: Route =
path("types" / "permissions") {
get {
complete(Permission.values.toSeq.sorted.map(_.toString))
}
}
@GET
@Path("types/pieces")
@Produces(value = Array("application/json"))
@Operation(summary = "pieces list", description = "Returns the available pieces",
responses = Array(
new ApiResponse(responseCode = "200", description = "List of available pieces",
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[String]))
))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("types"),
)
def getPieces: Route =
path("types" / "pieces") {
get {
complete(Piece.available)
}
}
}

View File

@ -15,13 +15,13 @@ import akka.http.scaladsl.server._
import akka.util.Timeout
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import javax.ws.rs.{Consumes, DELETE, GET, POST, PUT, Path, Produces}
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import io.swagger.v3.oas.annotations.{Operation, Parameter}
import javax.ws.rs._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.http.{Authorization, UserHelper}
import me.arcanis.ffxivbis.models.Permission
import scala.util.{Failure, Success}
@ -40,9 +40,12 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
responses = Array(
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"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "406", description = "Party with the specified ID already exists",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
tags = Array("party"),
)
@ -76,10 +79,14 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse])))),
responses = Array(
new ApiResponse(responseCode = "201", description = "User has been created"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "400", description = "Invalid parameters were supplied",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
@ -110,9 +117,12 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
),
responses = Array(
new ApiResponse(responseCode = "202", description = "User has been removed"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),
@ -143,9 +153,12 @@ class UserEndpoint(override val storage: ActorRef)(implicit timeout: Timeout)
content = Array(new Content(
array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])),
))),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid"),
new ApiResponse(responseCode = "403", description = "Access is forbidden"),
new ApiResponse(responseCode = "500", description = "Internal server error"),
new ApiResponse(responseCode = "401", description = "Supplied authorization is invalid",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "403", description = "Access is forbidden",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
new ApiResponse(responseCode = "500", description = "Internal server error",
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse])))),
),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))),
tags = Array("users"),

View File

@ -13,4 +13,4 @@ import io.swagger.v3.oas.annotations.media.Schema
case class PieceActionResponse(
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove")) action: ApiAction.Value,
@Schema(description = "piece description", required = true) piece: PieceResponse,
@Schema(description = "player description", required = true) playerIdResponse: PlayerIdResponse)
@Schema(description = "player description", required = true) playerId: PlayerIdResponse)

View File

@ -12,4 +12,4 @@ import io.swagger.v3.oas.annotations.media.Schema
case class PlayerActionResponse(
@Schema(description = "action to perform", required = true, `type` = "string", allowableValues = Array("add", "remove"), example = "add") action: ApiAction.Value,
@Schema(description = "player description", required = true) playerIdResponse: PlayerResponse)
@Schema(description = "player description", required = true) playerId: PlayerResponse)

View File

@ -37,6 +37,7 @@ class BasePartyView(override val storage: ActorRef)(implicit timeout: Timeout)
object BasePartyView {
import scalatags.Text
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def root(partyId: String): Text.TypedTag[String] =
a(href:=s"/party/$partyId", title:="root")("root")
@ -45,7 +46,7 @@ object BasePartyView {
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:=s"Party $partyId",
titleTag(s"Party $partyId"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -86,12 +86,13 @@ class BiSView(override val storage: ActorRef, ariyala: ActorRef)(implicit timeou
object BiSView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Best in slot",
titleTag("Best in slot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.util.Timeout
import me.arcanis.ffxivbis.http.UserHelper
import me.arcanis.ffxivbis.models.{Party, Permission, User}
import me.arcanis.ffxivbis.models.{Permission, User}
import scala.util.{Failure, Success}
@ -54,12 +54,13 @@ class IndexView(storage: ActorRef)(implicit timeout: Timeout)
object IndexView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template: String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(
head(
title:="FFXIV loot helper",
titleTag("FFXIV loot helper"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -71,12 +71,13 @@ class LootSuggestView(override val storage: ActorRef)(implicit timeout: Timeout)
object LootSuggestView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], piece: Option[Piece], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Suggest loot",
titleTag("Suggest loot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -79,12 +79,13 @@ class LootView (override val storage: ActorRef)(implicit timeout: Timeout)
object LootView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[Player], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Loot",
titleTag("Loot"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -14,7 +14,7 @@ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.util.Timeout
import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper}
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId, PlayerIdWithCounters}
import me.arcanis.ffxivbis.models._
import scala.concurrent.{ExecutionContext, Future}
@ -74,12 +74,13 @@ class PlayerView(override val storage: ActorRef, ariyala: ActorRef)(implicit tim
object PlayerView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Party",
titleTag("Party"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),
@ -92,7 +93,7 @@ object PlayerView {
form(action:=s"/party/$partyId/players", method:="post")(
input(name:="nick", id:="nick", placeholder:="nick", title:="nick", `type`:="nick"),
select(name:="job", id:="job", title:="job")
(for (job <- Job.jobs) yield option(job.toString)),
(for (job <- Job.available) yield option(job.toString)),
input(name:="link", id:="link", placeholder:="player bis link", title:="link", `type`:="text"),
input(name:="prioiry", id:="priority", placeholder:="priority", title:="priority", `type`:="number", value:="0"),
input(name:="action", id:="action", `type`:="hidden", value:="add"),

View File

@ -77,12 +77,13 @@ class UserView(override val storage: ActorRef)(implicit timeout: Timeout)
object UserView {
import scalatags.Text.all._
import scalatags.Text.tags2.{title => titleTag}
def template(partyId: String, users: Seq[User], error: Option[String]) =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
html(lang:="en",
head(
title:="Users",
titleTag("Users"),
link(rel:="stylesheet", `type`:="text/css", href:="/static/styles.css")
),

View File

@ -50,8 +50,8 @@ case class BiS(weapon: Option[Piece],
"ears" -> ears,
"neck" -> neck,
"wrist" -> wrist,
"leftRing" -> leftRing,
"rightRing" -> rightRing
"left ring" -> leftRing,
"right ring" -> rightRing
) + (name -> piece)
BiS(params)
}
@ -70,8 +70,8 @@ object BiS {
data.get("ears").flatten,
data.get("neck").flatten,
data.get("wrist").flatten,
data.get("leftRing").flatten,
data.get("rightRing").flatten)
data.get("left ring").flatten,
data.get("right ring").flatten)
def apply(): BiS = BiS(Seq.empty)

View File

@ -35,7 +35,7 @@ object Job {
def equality(objRepr: String): Boolean = objRepr match {
case _ if objRepr == AnyJob.toString => true
case _ if this.toString == AnyJob.toString => true
case _ => this.toString == obj.toString
case _ => this.toString == objRepr
}
canEqual(obj) && equality(obj.toString)
@ -96,8 +96,8 @@ object Job {
case object SMN extends Casters
case object RDM extends Casters
lazy val jobs: Seq[Job] =
lazy val available: Seq[Job] =
Seq(PLD, WAR, DRK, GNB, WHM, SCH, AST, MNK, DRG, NIN, SAM, BRD, MCH, DNC, BLM, SMN, RDM)
def withName(job: String): Job.Job = jobs.find(_.toString == job.toUpperCase).getOrElse(AnyJob)
def withName(job: String): Job.Job = available.find(_.toString == job.toUpperCase).getOrElse(AnyJob)
}

View File

@ -108,9 +108,7 @@ object Piece {
case "ears" => Ears(isTome, job)
case "neck" => Neck(isTome, job)
case "wrist" => Wrist(isTome, job)
case "ring" => Ring(isTome, job)
case "leftring" => Ring(isTome, job).copy(piece = "leftRing")
case "rightring" => Ring(isTome, job).copy(piece = "rightRing")
case ring @ ("ring" | "left ring" | "right ring") => Ring(isTome, job, ring)
case "accessory upgrade" => AccessoryUpgrade
case "body upgrade" => BodyUpgrade
case "weapon upgrade" => WeaponUpgrade
@ -119,5 +117,6 @@ object Piece {
lazy val available: Seq[String] = Seq("weapon",
"head", "body", "hands", "waist", "legs", "feet",
"ears", "neck", "wrist", "leftRing", "rightRing")
"ears", "neck", "wrist", "left ring", "right ring",
"accessory upgrade", "body upgrade", "weapon upgrade")
}

View File

@ -11,8 +11,8 @@ package me.arcanis.ffxivbis.service
import java.nio.file.Paths
import akka.actor.{Actor, Props}
import akka.http.scaladsl.model._
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.pattern.pipe
import akka.stream.ActorMaterializer
import akka.stream.scaladsl.{Keep, Sink}
@ -128,8 +128,8 @@ object Ariyala {
private def remapKey(key: String): Option[String] = key match {
case "mainhand" => Some("weapon")
case "chest" => Some("body")
case "ringLeft" => Some("leftRing")
case "ringRight" => Some("rightRing")
case "ringLeft" => Some("left ring")
case "ringRight" => Some("right ring")
case "head" | "hands" | "waist" | "legs" | "feet" | "ears" | "neck" | "wrist" => Some(key)
case _ => None
}

View File

@ -9,7 +9,7 @@
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{Job, Loot, Piece}
import slick.lifted.{ForeignKeyQuery, Index, PrimaryKey}
import slick.lifted.ForeignKeyQuery
import scala.concurrent.Future

View File

@ -9,7 +9,6 @@
package me.arcanis.ffxivbis.storage
import me.arcanis.ffxivbis.models.{BiS, Job, Player, PlayerId}
import slick.lifted.{Index, PrimaryKey}
import scala.concurrent.Future

View File

@ -15,8 +15,8 @@ object Fixtures {
Ears(isTome = false, Job.DNC),
Neck(isTome = true, Job.DNC),
Wrist(isTome = false, Job.DNC),
Ring(isTome = true, Job.DNC, "leftRing"),
Ring(isTome = true, Job.DNC, "rightRing")
Ring(isTome = true, Job.DNC, "left ring"),
Ring(isTome = true, Job.DNC, "right ring")
)
)
@ -30,8 +30,8 @@ object Fixtures {
lazy val lootLegs: Piece = Legs(isTome = false, Job.AnyJob)
lazy val lootEars: Piece = Ears(isTome = false, Job.AnyJob)
lazy val lootRing: Piece = Ring(isTome = true, Job.AnyJob)
lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "leftRing")
lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "rightRing")
lazy val lootLeftRing: Piece = Ring(isTome = true, Job.AnyJob, "left ring")
lazy val lootRightRing: Piece = Ring(isTome = true, Job.AnyJob, "right ring")
lazy val lootUpgrade: Piece = BodyUpgrade
lazy val loot: Seq[Piece] = Seq(lootBody, lootHands, lootLegs, lootUpgrade)

View File

@ -16,13 +16,13 @@ object Settings {
replace(default, values.toList)
}
def clearDatabase(config: Config): Unit = {
val databasePath =
config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":").last
val databaseFile = new File(databasePath)
if (databaseFile.exists)
databaseFile.delete()
}
def clearDatabase(config: Config): Unit =
config.getString("me.arcanis.ffxivbis.database.sqlite.db.url").split(":")
.lastOption.foreach { databasePath =>
val databaseFile = new File(databasePath)
if (databaseFile.exists)
databaseFile.delete()
}
def randomDatabasePath: String = File.createTempFile("ffxivdb-",".db").toPath.toString
def withRandomDatabase: Config =
config(Map("me.arcanis.ffxivbis.database.sqlite.db.url" -> s"jdbc:sqlite:$randomDatabasePath"))

View File

@ -0,0 +1,41 @@
package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.server._
import me.arcanis.ffxivbis.http.api.v1.json._
import me.arcanis.ffxivbis.models.{Job, Permission, Piece}
import org.scalatest.{Matchers, WordSpec}
import scala.language.postfixOps
class TypesEndpointTest extends WordSpec
with Matchers with ScalatestRouteTest with JsonSupport {
private val route: Route = new TypesEndpoint().route
"api v1 types endpoint" must {
"return all available jobs" in {
Get("/types/jobs") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual Job.available.map(_.toString)
}
}
"return all available permissions" in {
Get("/types/permissions") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual Permission.values.toSeq.sorted.map(_.toString)
}
}
"return all available pieces" in {
Get("/types/pieces") ~> route ~> check {
status shouldEqual StatusCodes.OK
responseAs[Seq[String]] shouldEqual Piece.available
}
}
}
}

View File

@ -7,7 +7,7 @@ class JobTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
"job model" must {
"create job from string" in {
Job.jobs.foreach { job =>
Job.available.foreach { job =>
Job.withName(job.toString) shouldEqual job
}
}
@ -17,7 +17,7 @@ class JobTest extends WordSpecLike with Matchers with BeforeAndAfterAll {
}
"equal AnyJob to others" in {
Job.jobs.foreach { job =>
Job.available.foreach { job =>
Job.AnyJob shouldBe job
}
}

View File

@ -1 +1 @@
version := "0.9.0"
version := "0.9.4"