use strict validator on input strings via api (#15)

It has been reported that that views are vulnerable for XSS because of
missing escaping (or validation). Instead of playing with conversion
from/to escaped/unescaped strings lets just forbid characters via api

This commit includes migration for postgres, sqlite migration is still
missing which will make it impossible to load pages for those parties.

This commit also includes several fixes:
* The issue when empty party could not be loaded
* The issue when link biis is not appllied after editing
* The issue when incorrect bis link has been saved
* The issue when empty password could be applied via api
* The issue when error message is not displayed at the index page

This commit also updates dependencies
This commit is contained in:
Evgenii Alekseev 2022-06-23 03:51:39 +03:00
parent 118d8faf6b
commit 0e8b95d0dd
30 changed files with 248 additions and 82 deletions

View File

@ -1,26 +1,26 @@
val AkkaVersion = "2.6.18" val AkkaVersion = "2.6.19"
val AkkaHttpVersion = "10.2.7" val AkkaHttpVersion = "10.2.9"
val ScalaTestVersion = "3.2.10" val ScalaTestVersion = "3.2.12"
val SlickVersion = "3.3.3" val SlickVersion = "3.3.3"
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10" libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11"
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4" libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5"
libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion
libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0" libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.7.0"
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.2" libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.3"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10" libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.10"
libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api") libraryDependencies += "com.zaxxer" % "HikariCP" % "5.0.1" exclude("org.slf4j", "slf4j-api")
libraryDependencies += "org.flywaydb" % "flyway-core" % "8.4.1" libraryDependencies += "org.flywaydb" % "flyway-core" % "8.5.12"
libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3"
libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.6"
libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4"
libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre"

View File

@ -0,0 +1,3 @@
update parties set party_alias = regexp_replace(party_alias, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
update players set nick = regexp_replace(nick, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');
update users set username = regexp_replace(username, '[^A-Za-z0-9!@#$%^&*()\-_=+;:'',./? ]', '', 'g');

View File

@ -9,9 +9,9 @@
<link rel="shortcut icon" href="/static/favicon.ico"> <link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
@ -157,11 +157,11 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script> <script src="/static/utils.js"></script>
<script src="/static/load.js"></script> <script src="/static/load.js"></script>

View File

@ -87,6 +87,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="/static/utils.js"></script>
<script src="/static/load.js"></script> <script src="/static/load.js"></script>
<script> <script>

View File

@ -9,9 +9,9 @@
<link rel="shortcut icon" href="/static/favicon.ico"> <link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
@ -174,11 +174,11 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script> <script src="/static/utils.js"></script>
<script src="/static/load.js"></script> <script src="/static/load.js"></script>

View File

@ -9,9 +9,9 @@
<link rel="shortcut icon" href="/static/favicon.ico"> <link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
@ -147,11 +147,11 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script> <script src="/static/utils.js"></script>
<script src="/static/load.js"></script> <script src="/static/load.js"></script>

View File

@ -9,9 +9,9 @@
<link rel="shortcut icon" href="/static/favicon.ico"> <link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.3/font/bootstrap-icons.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.css" type="text/css">
<link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
@ -141,11 +141,11 @@
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script> <script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/bootstrap-table.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/export/bootstrap-table-export.min.js"></script>
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script> <script src="https://unpkg.com/bootstrap-table@1.20.2/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
<script src="/static/utils.js"></script> <script src="/static/utils.js"></script>
<script src="/static/load.js"></script> <script src="/static/load.js"></script>

View File

@ -0,0 +1,16 @@
package me.arcanis.ffxivbis.http
import scala.collection.immutable.HashSet
trait ValidatorHelper {
def isValidString(string: String): Boolean = string.nonEmpty && string.forall(isValidSymbol)
def isValidSymbol(char: Char): Boolean =
char.isLetterOrDigit || ValidatorHelper.VALID_CHARACTERS.contains(char)
}
object ValidatorHelper {
final val VALID_CHARACTERS = HashSet.from("!@#$%^&*()-_=+;:',./? ")
}

View File

@ -49,7 +49,12 @@ class BiSEndpoint(
summary = "create best in slot", summary = "create best in slot",
description = "Create the best in slot set", description = "Create the best in slot set",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "player best in slot description", description = "player best in slot description",
@ -105,7 +110,12 @@ class BiSEndpoint(
summary = "get best in slot", summary = "get best in slot",
description = "Return the best in slot items", description = "Return the best in slot items",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter( new Parameter(
name = "nick", name = "nick",
in = ParameterIn.QUERY, in = ParameterIn.QUERY,
@ -167,7 +177,12 @@ class BiSEndpoint(
summary = "modify best in slot", summary = "modify best in slot",
description = "Add or remove an item from the best in slot", description = "Add or remove an item from the best in slot",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "action and piece description", description = "action and piece description",

View File

@ -46,7 +46,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "get loot list", summary = "get loot list",
description = "Return the looted items", description = "Return the looted items",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter( new Parameter(
name = "nick", name = "nick",
in = ParameterIn.QUERY, in = ParameterIn.QUERY,
@ -107,7 +112,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "modify loot list", summary = "modify loot list",
description = "Add or remove an item from the loot list", description = "Add or remove an item from the loot list",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "action and piece description", description = "action and piece description",
@ -164,7 +174,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "suggest loot", summary = "suggest loot",
description = "Suggest loot piece to party", description = "Suggest loot piece to party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "piece description", description = "piece description",

View File

@ -49,7 +49,12 @@ class PartyEndpoint(
summary = "get party description", summary = "get party description",
description = "Return the party description", description = "Return the party description",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
responses = Array( responses = Array(
new ApiResponse( new ApiResponse(
@ -96,7 +101,12 @@ class PartyEndpoint(
summary = "modify party description", summary = "modify party description",
description = "Edit party description", description = "Edit party description",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "new party description", description = "new party description",

View File

@ -50,7 +50,12 @@ class PlayerEndpoint(
summary = "get party", summary = "get party",
description = "Return the players who belong to the party", description = "Return the players who belong to the party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter( new Parameter(
name = "nick", name = "nick",
in = ParameterIn.QUERY, in = ParameterIn.QUERY,
@ -111,7 +116,12 @@ class PlayerEndpoint(
summary = "get party statistics", summary = "get party statistics",
description = "Return the party statistics", description = "Return the party statistics",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter( new Parameter(
name = "nick", name = "nick",
in = ParameterIn.QUERY, in = ParameterIn.QUERY,
@ -172,7 +182,12 @@ class PlayerEndpoint(
summary = "modify party", summary = "modify party",
description = "Add or remove a player from party list", description = "Add or remove a player from party list",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "player description", description = "player description",

View File

@ -96,7 +96,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "create new user", summary = "create new user",
description = "Add an user to the specified party", description = "Add an user to the specified party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
requestBody = new RequestBody( requestBody = new RequestBody(
description = "user description", description = "user description",
@ -151,7 +156,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "remove user", summary = "remove user",
description = "Remove an user from the specified party", description = "Remove an user from the specified party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"), new Parameter(name = "username", in = ParameterIn.PATH, description = "username to remove", example = "siuan"),
), ),
responses = Array( responses = Array(
@ -195,7 +205,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "get users", summary = "get users",
description = "Return the list of users belong to party", description = "Return the list of users belong to party",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
responses = Array( responses = Array(
new ApiResponse( new ApiResponse(
@ -246,7 +261,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
summary = "get current user", summary = "get current user",
description = "Return the current user descriptor", description = "Return the current user descriptor",
parameters = Array( parameters = Array(
new Parameter(name = "partyId", in = ParameterIn.PATH, description = "unique party ID", example = "abcdefgh"), new Parameter(
name = "partyId",
in = ParameterIn.PATH,
description = "unique party ID",
example = "o3KicHQPW5b0JcOm5yI3"
),
), ),
responses = Array( responses = Array(
new ApiResponse( new ApiResponse(

View File

@ -12,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PartyDescription import me.arcanis.ffxivbis.models.PartyDescription
case class PartyDescriptionModel( case class PartyDescriptionModel(
@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String, @Schema(description = "party id", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "party name") partyAlias: Option[String] @Schema(description = "party name") partyAlias: Option[String]
) { ) extends Validator {
require(partyAlias.forall(isValidString), stringMatchError("Party alias"))
def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) def toDescription: PartyDescription = PartyDescription(partyId, partyAlias)
} }

View File

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

View File

@ -17,4 +17,7 @@ case class PlayerBiSLinkModel(
example = "https://ffxiv.ariyala.com/19V5R" example = "https://ffxiv.ariyala.com/19V5R"
) link: String, ) link: String,
@Schema(description = "player description", required = true) playerId: PlayerIdModel @Schema(description = "player description", required = true) playerId: PlayerIdModel
) ) extends Validator {
require(isValidString(link), stringMatchError("BiS link"))
}

View File

@ -12,10 +12,14 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Job, PlayerId} import me.arcanis.ffxivbis.models.{Job, PlayerId}
case class PlayerIdModel( case class PlayerIdModel(
@Schema(description = "unique party ID. Required in responses", example = "abcdefgh") partyId: Option[String], @Schema(description = "unique party ID. Required in responses", example = "o3KicHQPW5b0JcOm5yI3") partyId: Option[
String
],
@Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String
) { ) extends Validator {
require(isValidString(nick), stringMatchError("Player name"))
def withPartyId(partyId: String): PlayerId = def withPartyId(partyId: String): PlayerId =
PlayerId(partyId, Job.withName(job), nick) PlayerId(partyId, Job.withName(job), nick)

View File

@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.PlayerIdWithCounters import me.arcanis.ffxivbis.models.PlayerIdWithCounters
case class PlayerIdWithCountersModel( case class PlayerIdWithCountersModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
@Schema(description = "is piece required by player or not", required = true) isRequired: Boolean, @Schema(description = "is piece required by player or not", required = true) isRequired: Boolean,

View File

@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{BiS, Job, Player} import me.arcanis.ffxivbis.models.{BiS, Job, Player}
case class PlayerModel( case class PlayerModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "job name", required = true, example = "DNC") job: String, @Schema(description = "job name", required = true, example = "DNC") job: String,
@Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "player nick name", required = true, example = "Siuan Sanche") nick: String,
@Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]], @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]],
@ -24,7 +24,10 @@ case class PlayerModel(
`type` = "number" `type` = "number"
) lootCountBiS: Option[Int], ) lootCountBiS: Option[Int],
@Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int], @Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int],
) { ) extends Validator {
require(isValidString(nick), stringMatchError("Player name"))
require(link.forall(isValidString), stringMatchError("BiS link"))
def toPlayer: Player = def toPlayer: Player =
Player( Player(

View File

@ -12,23 +12,30 @@ import io.swagger.v3.oas.annotations.media.Schema
import me.arcanis.ffxivbis.models.{Permission, User} import me.arcanis.ffxivbis.models.{Permission, User}
case class UserModel( case class UserModel(
@Schema(description = "unique party ID", required = true, example = "abcdefgh") partyId: String, @Schema(description = "unique party ID", required = true, example = "o3KicHQPW5b0JcOm5yI3") partyId: String,
@Schema(description = "username to login to party", required = true, example = "siuan") username: String, @Schema(description = "username to login to party", required = true, example = "siuan") username: String,
@Schema(description = "password to login to party", required = true, example = "pa55w0rd") password: String, @Schema(description = "password to login to party, required for user editing", example = "pa55w0rd") password: Option[
String
],
@Schema( @Schema(
description = "user permission", description = "user permission",
defaultValue = "get", defaultValue = "get",
`type` = "string", `type` = "string",
allowableValues = Array("get", "post", "admin") allowableValues = Array("get", "post", "admin")
) permission: Option[Permission.Value] = None ) permission: Option[Permission.Value] = None
) { ) extends Validator {
require(isValidString(username), stringMatchError("Username"))
require(password.forall(_.nonEmpty), "Password must not be empty")
def toUser: User = def toUser: User =
User(partyId, username, password, permission.getOrElse(Permission.get)) password.fold(throw new IllegalArgumentException("Password must noot be empty"))(
User(partyId, username, _, permission.getOrElse(Permission.get))
)
} }
object UserModel { object UserModel {
def fromUser(user: User): UserModel = def fromUser(user: User): UserModel =
UserModel(user.partyId, user.username, "", Some(user.permission)) UserModel(user.partyId, user.username, None, Some(user.permission))
} }

View File

@ -0,0 +1,9 @@
package me.arcanis.ffxivbis.http.api.v1.json
import me.arcanis.ffxivbis.http.ValidatorHelper
trait Validator extends ValidatorHelper {
def stringMatchError(what: String): String =
s"$what must contain only letters or digits or one of (${ValidatorHelper.VALID_CHARACTERS.mkString(", ")})"
}

View File

@ -45,13 +45,15 @@ trait BiSHelper extends BisProviderHelper {
timeout: Timeout, timeout: Timeout,
scheduler: Scheduler scheduler: Scheduler
): Future[Unit] = ): Future[Unit] =
storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ => storage
downloadBiS(link, playerId.job) .ask(RemovePiecesFromBiS(playerId, _))
.flatMap { bis => .flatMap { _ =>
Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) downloadBiS(link, playerId.job)
} .flatMap { bis =>
.map(_ => ()) Future.traverse(bis.pieces)(addPieceBiS(playerId, _))
} }
}
.flatMap(_ => storage.ask(UpdateBiSLink(playerId, link, _)))
def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] = def removePieceBiS(playerId: PlayerId, piece: Piece)(implicit timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage.ask(RemovePieceFromBiS(playerId, piece, _)) storage.ask(RemovePieceFromBiS(playerId, piece, _))

View File

@ -27,15 +27,15 @@ trait PlayerHelper extends BisProviderHelper {
)(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = )(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] =
storage storage
.ask(ref => AddPlayer(player, ref)) .ask(ref => AddPlayer(player, ref))
.map { res => .map { _ =>
player.link.map(_.trim).filter(_.nonEmpty) match { player.link.map(_.trim).filter(_.nonEmpty) match {
case Some(link) => case Some(link) =>
downloadBiS(link, player.job) downloadBiS(link, player.job)
.map { bis => .map { bis =>
bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _)))
} }
.map(_ => res) .flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _)))
case None => Future.successful(res) case None => Future.successful(())
} }
} }
.flatten .flatten

View File

@ -95,6 +95,11 @@ object DatabaseMessage {
override val isReadOnly: Boolean = false override val isReadOnly: Boolean = false
} }
case class UpdateBiSLink(playerId: PlayerId, link: String, actorRef: ActorRef[Unit]) extends PartyDatabaseMessage {
override val partyId: String = playerId.partyId
override val isReadOnly: Boolean = false
}
case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage {
override val partyId: String = partyDescription.partyId override val partyId: String = partyDescription.partyId
override val isReadOnly: Boolean = false override val isReadOnly: Boolean = false

View File

@ -62,6 +62,10 @@ trait DatabasePartyHandler { this: Database =>
run(profile.deletePlayer(playerId))(_ => client ! ()) run(profile.deletePlayer(playerId))(_ => client ! ())
Behaviors.same Behaviors.same
case UpdateBiSLink(playerId, link, client) =>
run(profile.updateBiSLink(playerId, link))(_ => client ! ())
Behaviors.same
case UpdateParty(description, client) => case UpdateParty(description, client) =>
run(profile.insertPartyDescription(description))(_ => client ! ()) run(profile.insertPartyDescription(description))(_ => client ! ())
Behaviors.same Behaviors.same

View File

@ -53,12 +53,14 @@ trait BiSProfile extends DatabaseConnection {
def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId))
def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] =
withConnection { implicit conn => if (playerIds.isEmpty) Future.successful(Seq.empty)
SQL("""select * from bis where player_id in ({player_ids})""") else
.on("player_ids" -> playerIds) withConnection { implicit conn =>
.executeQuery() SQL("""select * from bis where player_id in ({player_ids})""")
.as(loot.*) .on("player_ids" -> playerIds)
} .executeQuery()
.as(loot.*)
}
def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] =
withConnection { implicit conn => withConnection { implicit conn =>

View File

@ -59,12 +59,14 @@ trait LootProfile extends DatabaseConnection {
def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId))
def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] =
withConnection { implicit conn => if (playerIds.isEmpty) Future.successful(Seq.empty)
SQL("""select * from loot where player_id in ({player_ids})""") else
.on("player_ids" -> playerIds) withConnection { implicit conn =>
.executeQuery() SQL("""select * from loot where player_id in ({player_ids})""")
.as(loot.*) .on("player_ids" -> playerIds)
} .executeQuery()
.as(loot.*)
}
def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = def insertPieceById(loot: Loot)(playerId: Long): Future[Int] =
withConnection { implicit conn => withConnection { implicit conn =>

View File

@ -101,4 +101,18 @@ trait PlayersProfile extends DatabaseConnection {
.executeUpdate() .executeUpdate()
} }
def updateBiSLink(playerId: PlayerId, link: String): Future[Int] =
withConnection { implicit conn =>
SQL("""update players
| set bis_link = {link}
| where party_id = {party_id} and nick = {nick} and job = {job}""".stripMargin)
.on(
"link" -> link,
"party_id" -> playerId.partyId,
"nick" -> playerId.nick,
"job" -> playerId.job.toString
)
.executeUpdate()
}
} }

View File

@ -48,7 +48,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
"create a party" in { "create a party" in {
val uri = Uri(s"/party") val uri = Uri(s"/party")
val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Some(Fixtures.userPassword))
Post(uri, entity) ~> route ~> check { Post(uri, entity) ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
@ -57,7 +57,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
} }
"add user" in { "add user" in {
val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Some(Fixtures.userPassword2))
Post(endpoint, entity).withHeaders(auth) ~> route ~> check { Post(endpoint, entity).withHeaders(auth) ~> route ~> check {
status shouldEqual StatusCodes.Accepted status shouldEqual StatusCodes.Accepted

View File

@ -66,6 +66,18 @@ class DatabasePartyHandlerTest extends ScalaTestWithActorTestKit(Settings.withRa
Compare.seqEquals(party.getPlayers, Seq(newPlayer)) shouldEqual true Compare.seqEquals(party.getPlayers, Seq(newPlayer)) shouldEqual true
} }
"update bis link" in {
val updateProbe = testKit.createTestProbe[Unit]()
val newPlayer = Fixtures.playerEmpty.copy(priority = 2, link = Some("link"))
database ! UpdateBiSLink(Fixtures.playerEmpty.playerId, "link", updateProbe.ref)
updateProbe.expectMessage(askTimeout, ())
val probe = testKit.createTestProbe[Option[Player]]()
database ! GetPlayer(Fixtures.playerEmpty.playerId, probe.ref)
probe.expectMessage(askTimeout, Some(newPlayer))
}
"remove player" in { "remove player" in {
val updateProbe = testKit.createTestProbe[Unit]() val updateProbe = testKit.createTestProbe[Unit]()
database ! RemovePlayer(Fixtures.playerEmpty.playerId, updateProbe.ref) database ! RemovePlayer(Fixtures.playerEmpty.playerId, updateProbe.ref)