From 0e8b95d0dd1e290839f5add2c8198f4423262274 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Thu, 23 Jun 2022 03:51:39 +0300 Subject: [PATCH] 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 --- libraries.sbt | 18 ++++++------ .../V8_0__Remove_special_characters.sql | 3 ++ src/main/resources/html/bis.html | 10 +++---- src/main/resources/html/index.html | 1 + src/main/resources/html/loot.html | 10 +++---- src/main/resources/html/party.html | 10 +++---- src/main/resources/html/users.html | 10 +++---- .../ffxivbis/http/ValidatorHelper.scala | 16 +++++++++++ .../ffxivbis/http/api/v1/BiSEndpoint.scala | 21 ++++++++++++-- .../ffxivbis/http/api/v1/LootEndpoint.scala | 21 ++++++++++++-- .../ffxivbis/http/api/v1/PartyEndpoint.scala | 14 ++++++++-- .../ffxivbis/http/api/v1/PlayerEndpoint.scala | 21 ++++++++++++-- .../ffxivbis/http/api/v1/UserEndpoint.scala | 28 ++++++++++++++++--- .../api/v1/json/PartyDescriptionModel.scala | 6 ++-- .../http/api/v1/json/PartyIdModel.scala | 4 ++- .../http/api/v1/json/PlayerBiSLinkModel.scala | 5 +++- .../http/api/v1/json/PlayerIdModel.scala | 8 ++++-- .../v1/json/PlayerIdWithCountersModel.scala | 2 +- .../http/api/v1/json/PlayerModel.scala | 7 +++-- .../ffxivbis/http/api/v1/json/UserModel.scala | 17 +++++++---- .../ffxivbis/http/api/v1/json/Validator.scala | 9 ++++++ .../ffxivbis/http/helpers/BiSHelper.scala | 16 ++++++----- .../ffxivbis/http/helpers/PlayerHelper.scala | 6 ++-- .../ffxivbis/messages/DatabaseMessage.scala | 5 ++++ .../database/impl/DatabasePartyHandler.scala | 4 +++ .../arcanis/ffxivbis/storage/BiSProfile.scala | 14 ++++++---- .../ffxivbis/storage/LootProfile.scala | 14 ++++++---- .../ffxivbis/storage/PlayersProfile.scala | 14 ++++++++++ .../http/api/v1/UserEndpointTest.scala | 4 +-- .../database/DatabasePartyHandlerTest.scala | 12 ++++++++ 30 files changed, 248 insertions(+), 82 deletions(-) create mode 100644 src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala create mode 100644 src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala diff --git a/libraries.sbt b/libraries.sbt index 1e87d66..d1d2463 100644 --- a/libraries.sbt +++ b/libraries.sbt @@ -1,26 +1,26 @@ -val AkkaVersion = "2.6.18" -val AkkaHttpVersion = "10.2.7" -val ScalaTestVersion = "3.2.10" +val AkkaVersion = "2.6.19" +val AkkaHttpVersion = "10.2.9" +val ScalaTestVersion = "3.2.12" val SlickVersion = "3.3.3" -libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.10" -libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4" +libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.11" +libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" libraryDependencies += "com.typesafe.akka" %% "akka-http" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % 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 += "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 += "org.playframework.anorm" %% "anorm" % "2.6.10" 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.postgresql" % "postgresql" % "42.3.1" +libraryDependencies += "org.postgresql" % "postgresql" % "42.3.6" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" diff --git a/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql b/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql new file mode 100644 index 0000000..6535f12 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V8_0__Remove_special_characters.sql @@ -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'); \ No newline at end of file diff --git a/src/main/resources/html/bis.html b/src/main/resources/html/bis.html index 1013e71..976f414 100644 --- a/src/main/resources/html/bis.html +++ b/src/main/resources/html/bis.html @@ -9,9 +9,9 @@ - + - + @@ -157,11 +157,11 @@ - + - + - + diff --git a/src/main/resources/html/index.html b/src/main/resources/html/index.html index 9653836..14664bd 100644 --- a/src/main/resources/html/index.html +++ b/src/main/resources/html/index.html @@ -87,6 +87,7 @@ + - + - + - + diff --git a/src/main/resources/html/party.html b/src/main/resources/html/party.html index a5cbecd..0c5719f 100644 --- a/src/main/resources/html/party.html +++ b/src/main/resources/html/party.html @@ -9,9 +9,9 @@ - + - + @@ -147,11 +147,11 @@ - + - + - + diff --git a/src/main/resources/html/users.html b/src/main/resources/html/users.html index ac930f8..2f2c33c 100644 --- a/src/main/resources/html/users.html +++ b/src/main/resources/html/users.html @@ -9,9 +9,9 @@ - + - + @@ -141,11 +141,11 @@ - + - + - + diff --git a/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala new file mode 100644 index 0000000..d9d4f5f --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/ValidatorHelper.scala @@ -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("!@#$%^&*()-_=+;:',./? ") +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala index e2f6f81..a725cc9 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpoint.scala @@ -49,7 +49,12 @@ class BiSEndpoint( summary = "create best in slot", description = "Create the best in slot set", 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( description = "player best in slot description", @@ -105,7 +110,12 @@ class BiSEndpoint( summary = "get best in slot", description = "Return the best in slot items", 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 = "nick", in = ParameterIn.QUERY, @@ -167,7 +177,12 @@ class BiSEndpoint( summary = "modify best in slot", description = "Add or remove an item from the best in slot", 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( description = "action and piece description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala index c1bdbce..75cb5f5 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpoint.scala @@ -46,7 +46,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get loot list", description = "Return the looted items", 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 = "nick", in = ParameterIn.QUERY, @@ -107,7 +112,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "modify loot list", description = "Add or remove an item from the loot list", 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( description = "action and piece description", @@ -164,7 +174,12 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "suggest loot", description = "Suggest loot piece to party", 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( description = "piece description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala index 6f7f86f..023c316 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpoint.scala @@ -49,7 +49,12 @@ class PartyEndpoint( summary = "get party description", description = "Return the party description", 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( new ApiResponse( @@ -96,7 +101,12 @@ class PartyEndpoint( summary = "modify party description", description = "Edit party description", 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( description = "new party description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala index b2d37d0..5b9849e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpoint.scala @@ -50,7 +50,12 @@ class PlayerEndpoint( summary = "get party", description = "Return the players who belong to the party", 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 = "nick", in = ParameterIn.QUERY, @@ -111,7 +116,12 @@ class PlayerEndpoint( summary = "get party statistics", description = "Return the party statistics", 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 = "nick", in = ParameterIn.QUERY, @@ -172,7 +182,12 @@ class PlayerEndpoint( summary = "modify party", description = "Add or remove a player from party list", 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( description = "player description", diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala index 8b05666..6ce3011 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpoint.scala @@ -96,7 +96,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "create new user", description = "Add an user to the specified party", 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( description = "user description", @@ -151,7 +156,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "remove user", description = "Remove an user from the specified party", 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"), ), responses = Array( @@ -195,7 +205,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get users", description = "Return the list of users belong to party", 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( new ApiResponse( @@ -246,7 +261,12 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A summary = "get current user", description = "Return the current user descriptor", 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( new ApiResponse( diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala index bfb5b2b..26dd085 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala @@ -12,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PartyDescription 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] -) { +) extends Validator { + + require(partyAlias.forall(isValidString), stringMatchError("Party alias")) def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala index 88fc028..7d5a435 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala @@ -10,4 +10,6 @@ package me.arcanis.ffxivbis.http.api.v1.json 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 +) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala index 7dfc244..e62f366 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala @@ -17,4 +17,7 @@ case class PlayerBiSLinkModel( example = "https://ffxiv.ariyala.com/19V5R" ) link: String, @Schema(description = "player description", required = true) playerId: PlayerIdModel -) +) extends Validator { + + require(isValidString(link), stringMatchError("BiS link")) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala index efca7e6..8ee4b13 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala @@ -12,10 +12,14 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, PlayerId} 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 = "player nick name", required = true, example = "Siuan Sanche") nick: String -) { +) extends Validator { + + require(isValidString(nick), stringMatchError("Player name")) def withPartyId(partyId: String): PlayerId = PlayerId(partyId, Job.withName(job), nick) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala index df72789..1623ce7 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PlayerIdWithCounters 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 = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "is piece required by player or not", required = true) isRequired: Boolean, diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala index 515d17d..2d55972 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala @@ -12,7 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{BiS, Job, Player} 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 = "player nick name", required = true, example = "Siuan Sanche") nick: String, @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]], @@ -24,7 +24,10 @@ case class PlayerModel( `type` = "number" ) lootCountBiS: 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 = Player( diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala index 437d940..19306f2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala @@ -12,23 +12,30 @@ import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Permission, User} 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 = "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( description = "user permission", defaultValue = "get", `type` = "string", allowableValues = Array("get", "post", "admin") ) permission: Option[Permission.Value] = None -) { +) extends Validator { + + require(isValidString(username), stringMatchError("Username")) + require(password.forall(_.nonEmpty), "Password must not be empty") 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 { def fromUser(user: User): UserModel = - UserModel(user.partyId, user.username, "", Some(user.permission)) + UserModel(user.partyId, user.username, None, Some(user.permission)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala new file mode 100644 index 0000000..fa68852 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/Validator.scala @@ -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(", ")})" +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala index 0cdcd1d..c4c2188 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala @@ -45,13 +45,15 @@ trait BiSHelper extends BisProviderHelper { timeout: Timeout, scheduler: Scheduler ): Future[Unit] = - storage.ask(RemovePiecesFromBiS(playerId, _)).flatMap { _ => - downloadBiS(link, playerId.job) - .flatMap { bis => - Future.traverse(bis.pieces)(addPieceBiS(playerId, _)) - } - .map(_ => ()) - } + storage + .ask(RemovePiecesFromBiS(playerId, _)) + .flatMap { _ => + downloadBiS(link, playerId.job) + .flatMap { bis => + 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] = storage.ask(RemovePieceFromBiS(playerId, piece, _)) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala index a19ad30..1047eae 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala @@ -27,15 +27,15 @@ trait PlayerHelper extends BisProviderHelper { )(implicit executionContext: ExecutionContext, timeout: Timeout, scheduler: Scheduler): Future[Unit] = storage .ask(ref => AddPlayer(player, ref)) - .map { res => + .map { _ => player.link.map(_.trim).filter(_.nonEmpty) match { case Some(link) => downloadBiS(link, player.job) .map { bis => bis.pieces.map(piece => storage.ask(AddPieceToBis(player.playerId, piece, _))) } - .map(_ => res) - case None => Future.successful(res) + .flatMap(_ => storage.ask(UpdateBiSLink(player.playerId, link, _))) + case None => Future.successful(()) } } .flatten diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala index 33b5aeb..3f291c2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala @@ -95,6 +95,11 @@ object DatabaseMessage { 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 { override val partyId: String = partyDescription.partyId override val isReadOnly: Boolean = false diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala index 2e679bd..07afc0a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabasePartyHandler.scala @@ -62,6 +62,10 @@ trait DatabasePartyHandler { this: Database => run(profile.deletePlayer(playerId))(_ => client ! ()) Behaviors.same + case UpdateBiSLink(playerId, link, client) => + run(profile.updateBiSLink(playerId, link))(_ => client ! ()) + Behaviors.same + case UpdateParty(description, client) => run(profile.insertPartyDescription(description))(_ => client ! ()) Behaviors.same diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index 20c6676..7663c81 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -53,12 +53,14 @@ trait BiSProfile extends DatabaseConnection { def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = - withConnection { implicit conn => - SQL("""select * from bis where player_id in ({player_ids})""") - .on("player_ids" -> playerIds) - .executeQuery() - .as(loot.*) - } + if (playerIds.isEmpty) Future.successful(Seq.empty) + else + withConnection { implicit conn => + SQL("""select * from bis where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = withConnection { implicit conn => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index a873faa..4c8116d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -59,12 +59,14 @@ trait LootProfile extends DatabaseConnection { def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = - withConnection { implicit conn => - SQL("""select * from loot where player_id in ({player_ids})""") - .on("player_ids" -> playerIds) - .executeQuery() - .as(loot.*) - } + if (playerIds.isEmpty) Future.successful(Seq.empty) + else + withConnection { implicit conn => + SQL("""select * from loot where player_id in ({player_ids})""") + .on("player_ids" -> playerIds) + .executeQuery() + .as(loot.*) + } def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = withConnection { implicit conn => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index 4b0758c..4921fed 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -101,4 +101,18 @@ trait PlayersProfile extends DatabaseConnection { .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() + } + } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala index b00c6e1..2fe7ae5 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/UserEndpointTest.scala @@ -48,7 +48,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "create a party" in { 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 { status shouldEqual StatusCodes.OK @@ -57,7 +57,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } "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 { status shouldEqual StatusCodes.Accepted diff --git a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala index aac8d1f..9fdf97e 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/database/DatabasePartyHandlerTest.scala @@ -66,6 +66,18 @@ class DatabasePartyHandlerTest extends ScalaTestWithActorTestKit(Settings.withRa 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 { val updateProbe = testKit.createTestProbe[Unit]() database ! RemovePlayer(Fixtures.playerEmpty.playerId, updateProbe.ref)