diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e21274e..85c0f89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,9 +25,9 @@ jobs: uses: actions/setup-java@v2 with: distribution: temurin - java-version: 8 + java-version: 17 - name: create dist - run: sbt -v dist + run: make dist - name: release uses: softprops/action-gh-release@v1 with: diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d83ea63..bae636a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -17,6 +17,6 @@ jobs: uses: actions/setup-java@v2 with: distribution: temurin - java-version: 8 + java-version: 17 - name: run tests - run: sbt -v +test \ No newline at end of file + run: make tests \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f2eee08 --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +.PHONY: check clean compile dist push tests version +.DEFAULT_GOAL := compile + +PROJECT := ffxivbis + +check: + sbt scalafmtCheck + +clean: + sbt clean + +compile: clean + sbt compile + +format: + sbt scalafmt + +dist: tests version + sbt dist + +push: dist + git add version.sbt + git commit -m "Release $(VERSION)" + git tag "$(VERSION)" + git push + git push --tags + +tests: compile check + sbt test + +version: +ifndef VERSION + $(error VERSION is required, but not set) +endif + sed -i '/version := "[0-9.]*/s/[^"][^)]*/version := "$(VERSION)"/' version.sbt diff --git a/libraries.sbt b/libraries.sbt index 7f38267..cc09bee 100644 --- a/libraries.sbt +++ b/libraries.sbt @@ -14,7 +14,6 @@ libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % " libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" -libraryDependencies += "com.lihaoyi" %% "scalatags" % "0.9.2" libraryDependencies += "com.typesafe.slick" %% "slick" % SlickVersion libraryDependencies += "com.typesafe.slick" %% "slick-hikaricp" % SlickVersion @@ -23,6 +22,8 @@ libraryDependencies += "org.xerial" % "sqlite-jdbc" % "3.36.0.3" libraryDependencies += "org.postgresql" % "postgresql" % "42.3.1" libraryDependencies += "org.mindrot" % "jbcrypt" % "0.4" +libraryDependencies += "com.google.guava" % "guava" % "31.0.1-jre" + // testing libraryDependencies += "org.scalactic" %% "scalactic" % ScalaTestVersion % "test" diff --git a/src/main/resources/html/bis.html b/src/main/resources/html/bis.html new file mode 100644 index 0000000..28032d5 --- /dev/null +++ b/src/main/resources/html/bis.html @@ -0,0 +1,349 @@ + + + + + Best in slot + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Best in slot

+
+ +
+
+ + + +
+ + + + + + + + + + + +
nickjobpiecepiece type
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/index.html b/src/main/resources/html/index.html new file mode 100644 index 0000000..c524f14 --- /dev/null +++ b/src/main/resources/html/index.html @@ -0,0 +1,183 @@ + + + + + FFXIV loot helper + + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+
+
+ +
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/loot.html b/src/main/resources/html/loot.html new file mode 100644 index 0000000..8996a2b --- /dev/null +++ b/src/main/resources/html/loot.html @@ -0,0 +1,338 @@ + + + + + Loot table + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Looted items

+
+ +
+
+ + + +
+ + + + + + + + + + + + + +
nickjobpiecepiece typeis free lootdate
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/party.html b/src/main/resources/html/party.html new file mode 100644 index 0000000..b151f36 --- /dev/null +++ b/src/main/resources/html/party.html @@ -0,0 +1,258 @@ + + + + + FFXIV loot helper + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+ + + +
+ + + + + + + + + + + + + +
nickjobbest in slot linktotal bis pieces lootedtotal pieces lootedpriority
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/html/redoc.html b/src/main/resources/html/redoc.html new file mode 100644 index 0000000..6b67c4d --- /dev/null +++ b/src/main/resources/html/redoc.html @@ -0,0 +1,19 @@ + + + + + FFXIV loot helper API + + + + + + + + + + + + + + diff --git a/src/main/resources/html/swagger.html b/src/main/resources/html/swagger.html deleted file mode 100644 index cdee79d..0000000 --- a/src/main/resources/html/swagger.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - FFXIV loot tracker API - - - - - - - - - - - - - diff --git a/src/main/resources/html/users.html b/src/main/resources/html/users.html new file mode 100644 index 0000000..3e28c9f --- /dev/null +++ b/src/main/resources/html/users.html @@ -0,0 +1,230 @@ + + + + + User management + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+

Users

+
+ +
+
+ + + +
+ + + + + + + + + +
usernamepermission
+
+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index fc8f9f1..7701121 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -1,62 +1,71 @@ me.arcanis.ffxivbis { - bis-provider { - include "item_data.json" + bis-provider { + include "item_data.json" - # xivapi base url, string, required - xivapi-url = "https://xivapi.com" - # xivapi developer key, string, optional - #xivapi-key = "abcdef" - } - - database { - # database section. Section must be declared inside - # for more detailed section descriptions refer to slick documentation - mode = "sqlite" - - sqlite { - profile = "slick.jdbc.SQLiteProfile$" - db { - url = "jdbc:sqlite:ffxivbis.db" - user = "user" - password = "password" - } - numThreads = 10 + # xivapi base url, string, required + xivapi-url = "https://xivapi.com" + # xivapi developer key, string, optional + #xivapi-key = "abcdef" } - postgresql { - profile = "slick.jdbc.PostgresProfile$" - db { - url = "jdbc:postgresql://localhost/ffxivbis" - user = "ffxivbis" - password = "ffxivbis" + database { + # database section. Section must be declared inside + # for more detailed section descriptions refer to slick documentation + mode = "sqlite" - connectionPool = disabled - keepAliveConnection = yes - } - numThreads = 10 + sqlite { + profile = "slick.jdbc.SQLiteProfile$" + db { + url = "jdbc:sqlite:ffxivbis.db" + #user = "user" + #password = "password" + } + numThreads = 10 + } + + postgresql { + profile = "slick.jdbc.PostgresProfile$" + db { + url = "jdbc:postgresql://localhost/ffxivbis" + #user = "ffxivbis" + #password = "ffxivbis" + + connectionPool = disabled + keepAliveConnection = yes + } + numThreads = 10 + } } - } - settings { - # counters of Player class which will be called to sort players for loot priority - # list of strings, required - priority = [ - "isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal" - ] - # general request timeout, duratin, required - request-timeout = 10s - # party in-memory storage lifetime - cache-timeout = 1m - } + settings { + # counters of Player class which will be called to sort players for loot priority + # list of strings, required + priority = [ + "isRequired", "lootCountBiS", "priority", "lootCount", "lootCountTotal" + ] + # general request timeout, duratin, required + request-timeout = 10s + # party in-memory storage lifetime + cache-timeout = 1m + } - web { - # address to bind, string, required - host = "127.0.0.1" - # port to bind, int, required - port = 8000 - # hostname to use in docs, if not set host:port will be used - #hostname = "127.0.0.1:8000" + web { + # address to bind, string, required + host = "127.0.0.1" + # port to bind, int, required + port = 8000 + # hostname to use in docs, if not set host:port will be used + #hostname = "127.0.0.1:8000" + # enable head requests for GET requests + enable-head-requests = yes + + authorization-cache { + # maximum amount of cached logins + cache-size = 1024 + # ttl of cached logins + cache-timeout = 1m + } } default-dispatcher { diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..78d37d6 Binary files /dev/null and b/src/main/resources/static/favicon.ico differ diff --git a/src/main/resources/static/load.js b/src/main/resources/static/load.js new file mode 100644 index 0000000..264148f --- /dev/null +++ b/src/main/resources/static/load.js @@ -0,0 +1,54 @@ +function loadHeader(partyId) { + const title = $("#navbar-title"); + + // because I don't know how to handle relative url if current does not end with slash + title.attr("href", `/party/${partyId}`); + $("#navbar-bis").attr("href", `/party/${partyId}/bis`); + $("#navbar-loot").attr("href", `/party/${partyId}/loot`); + $("#navbar-users").attr("href", `/party/${partyId}/users`); + + $.ajax({ + url: `/api/v1/party/${partyId}/description`, + type: "GET", + dataType: "json", + success: function (resp) { + title.text(safe(resp.partyAlias || partyId)); + }, + error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, + }); +} + +function loadTypes(url, selector) { + $.ajax({ + url: url, + type: "GET", + dataType: "json", + success: function (data) { + const options = data.map(function (name) { + const option = document.createElement("option"); + option.value = name; + option.innerText = name; + return option; + }); + selector.empty().append(options); + }, + error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, + }); +} + +function setupFormClear(dialog, reset) { + dialog.on("shown.bs.modal", function () { + $(this).find("form").trigger("reset"); + $(this).find("table").bootstrapTable("removeAll"); + if (reset) { + reset(); + } + }); +} + +function setupRemoveButton(table, removeButton) { + table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", + function () { + removeButton.prop("disabled", !table.bootstrapTable("getSelections").length); + }); +} diff --git a/src/main/resources/static/styles.css b/src/main/resources/static/styles.css index a24324d..e69de29 100644 --- a/src/main/resources/static/styles.css +++ b/src/main/resources/static/styles.css @@ -1,277 +0,0 @@ -/* in-text images */ -figure.img { - float: right; - border: 0px solid #333; - padding: 0px; - margin: 5px 0px 5px 10px; -} -figure.img img { - max-width: 100%; - height: auto; -} -figure.img figcaption { - margin: 0px; - font-size: 90%; - font-style: italic; - text-align: center; -} - -h1 .octicon-link, h2 .octicon-link, h3 .octicon-link, h4 .octicon-link, h5 .octicon-link, h6 .octicon-link { - display: none; - color: #222222; - vertical-align: middle; -} - -h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor{ - padding-left: 8px; - margin-left: -24px; - text-decoration: none; -} - -h1:hover .anchor .octicon-link, h2:hover .anchor .octicon-link, h3:hover .anchor .octicon-link, h4:hover .anchor .octicon-link, h5:hover .anchor .octicon-link, h6:hover .anchor .octicon-link { - display: inline-block; -} - -body { - padding: 50px; - font: 14px/1.5 "Liberation Sans", Helvetica, Arial, sans-serif; - color: #555555; - background: #eaeaea -} - -h1, h2, h3, h4, h5, h6 { - color: #222222; - margin: 0 0 20px; -} - -p, ul, ol, table, pre, dl { - margin: 0 0 20px; - text-align: justify; -} - -h1, h2, h3 { - line-height: 1.1; -} - -h1 { - font-size: 28px; -} - -h2 { - color: #393939; -} - -h3, h4, h5, h6 { - color: #494949; -} - -a { - color: #3399cc; - font-weight: 350; - text-decoration: none; -} - -a small { - font-size: 11px; - color: #777777; - margin-top: -0.6em; - display: block; -} - -.wrapper { - width: 80%; - margin: 0 auto; -} - -blockquote { - border-left: 1px solid #ffffff; - margin: 0; - padding: 0 0 0 20px; - font-style: italic; -} - -code, pre { - font-family: "Liberation Mono", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; - color: #222222; - font-size: 12px; -} - -pre { - padding: 8px 15px; - border-radius: 5px; - border: 1px solid #e5e5e5; - overflow-x: auto; - overflow-y: auto; -} - -input, select{ - box-sizing: border-box; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, td { - padding: 5px 10px; - border-bottom: 1px solid #ffffff; -} - -td { - text-align: justify; -} - -dt { - color: #444444; - font-weight: 700; -} - -th { - text-align: left; - color: #444444; -} - -img { - max-width: 100%; -} - -header { - width: 20%; - float: left; - position: fixed; -} - -header ul { - list-style: none; - height: 40px; - padding: 0; - background: #eeeeee; - border-radius: 5px; - border: 1px solid #d2d2d2; - box-shadow: inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; - width: 15%; -} - -header li { - width: 8%; - float: left; - border-right: 1px solid #d2d2d2; - height: 40px; -} - -header ul a { - line-height: 1; - font-size: 11px; - color: #999999; - display: block; - text-align: center; - padding-top: 6px; - height: 40px; -} - -strong { - color: #222222; - font-weight: 700; -} - -header ul li + li { - width: 8%; - border-left: 1px solid #ffffff; -} - -header ul li + li + li { - width: 8%; - border-right: none; -} - -header ul a strong { - font-size: 14px; - display: block; - color: #222222; -} - -section { - width: 70%; - float: right; - padding-bottom: 50px; -} - -small { - font-size: 11px; -} - -hr { - border: 0; - background: #ffffff; - height: 1px; - margin: 0 0 20px; -} - -footer { - width: 20%; - float: left; - position: fixed; - bottom: 50px; -} - -@media print, screen and (max-width: 960px) { - div.wrapper { - width: auto; - margin: 0; - } - header, section, footer { - float: none; - position: static; - width: auto; - } - header { - padding-right: 320px; - } - section { - border: 1px solid #e5e5e5; - border-width: 1px 0; - padding: 20px 0; - margin: 0 0 20px; - } - header a small { - display: inline; - } - header ul { - position: absolute; - right: 50px; - top: 52px; - } -} - -@media print, screen and (max-width: 720px) { - body { - word-wrap: break-word; - } - header { - padding: 0; - } - header ul, header p.view { - position: static; - } - pre, code { - word-wrap: normal; - } -} - -@media print, screen and (max-width: 480px) { - body { - padding: 15px; - } - header ul { - display: none; - } -} - -@media print { - body { - padding: 0.4in; - font-size: 12pt; - color: #444444; - } -} diff --git a/src/main/resources/static/table_export.js b/src/main/resources/static/table_export.js deleted file mode 100644 index ea019df..0000000 --- a/src/main/resources/static/table_export.js +++ /dev/null @@ -1,31 +0,0 @@ -function downloadCsv(csv, filename) { - var csvFile = new Blob([csv], {"type": "text/csv"}); - - var downloadLink = document.createElement("a"); - downloadLink.download = filename; - downloadLink.href = window.URL.createObjectURL(csvFile); - downloadLink.style.display = "none"; - - document.body.appendChild(downloadLink); - downloadLink.click(); -} - -function exportTableToCsv(filename) { - var table = document.getElementById("result"); - var rows = table.getElementsByTagName("tr"); - - var csv = []; - for (var i = 0; i < rows.length; i++) { - if (rows[i].style.display === "none") - continue; - var cols = rows[i].querySelectorAll("td, th"); - - var row = []; - for (var j = 0; j < cols.length; j++) - row.push(cols[j].innerText); - - csv.push(row.join(",")); - } - - downloadCsv(csv.join("\n"), filename); -} diff --git a/src/main/resources/static/table_search.js b/src/main/resources/static/table_search.js deleted file mode 100644 index a2eec3b..0000000 --- a/src/main/resources/static/table_search.js +++ /dev/null @@ -1,21 +0,0 @@ -function searchTable() { - var input = document.getElementById("search"); - var filter = input.value.toLowerCase(); - var table = document.getElementById("result"); - var tr = table.getElementsByTagName("tr"); - - // from 1 coz of header - for (var i = 1; i < tr.length; i++) { - var td = tr[i].getElementsByClassName("include_search"); - var display = "none"; - for (var j = 0; j < td.length; j++) { - if (td[j].tagName.toLowerCase() === "td") { - if (td[j].innerHTML.toLowerCase().indexOf(filter) > -1) { - display = ""; - break; - } - } - } - tr[i].style.display = display; - } -} diff --git a/src/main/resources/static/utils.js b/src/main/resources/static/utils.js new file mode 100644 index 0000000..63bc91c --- /dev/null +++ b/src/main/resources/static/utils.js @@ -0,0 +1,44 @@ +function createAlert(message, placeholder) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = ``; + placeholder.append(wrapper); +} + +function formatPlayerId(obj) { + return `${obj.nick} (${obj.job})`; +} + +function getCurrentOption(select) { + return select.find(":selected")[0]; +} + +function getPartyId() { + const request = new XMLHttpRequest(); + request.open("HEAD", document.location, false); + request.send(null); + + // tuple lol + return [ + request.getResponseHeader("X-Party-Id"), + request.getResponseHeader("X-User-Permission") === "get", + ] +} + +function requestAlert(jqXHR, errorThrown) { + let message; + try { + message = $.parseJSON(jqXHR.responseText).message; + } catch (_) { + message = errorThrown; + } + const alert = $("#alert-placeholder"); + createAlert(`Error during request: ${message}`, alert); +} + +function safe(string) { + return String(string) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} diff --git a/src/main/resources/swagger-info/description.md b/src/main/resources/swagger-info/description.md index 39647dc..a2b184f 100644 --- a/src/main/resources/swagger-info/description.md +++ b/src/main/resources/swagger-info/description.md @@ -11,6 +11,8 @@ REST json API description to interact with FFXIVBiS service. # Limitations +No limitations for the API so far. + # Authentication For the most party utils service requires user to be authenticated. User permission can be one of `get`, `post` or `admin`. diff --git a/src/main/scala/me/arcanis/ffxivbis/Application.scala b/src/main/scala/me/arcanis/ffxivbis/Application.scala index d91737c..951b91d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/Application.scala +++ b/src/main/scala/me/arcanis/ffxivbis/Application.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,16 +8,16 @@ */ package me.arcanis.ffxivbis -import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{Behavior, PostStop, Signal} import akka.http.scaladsl.Http import akka.http.scaladsl.server.Route import akka.stream.Materializer import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.http.RootEndpoint +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration import scala.concurrent.ExecutionContext diff --git a/src/main/scala/me/arcanis/ffxivbis/Configuration.scala b/src/main/scala/me/arcanis/ffxivbis/Configuration.scala new file mode 100644 index 0000000..f92bf8e --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/Configuration.scala @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019-2022 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 + +import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory} + +object Configuration { + + def load(): Config = { + val root = ConfigFactory.load() + root + .withValue( + "akka.http.server.transparent-head-requests", + ConfigValueFactory.fromAnyRef(root.getBoolean("me.arcanis.ffxivbis.web.enable-head-requests")) + ) + } +} diff --git a/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala b/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala index 6ad9732..15db400 100644 --- a/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala +++ b/src/main/scala/me/arcanis/ffxivbis/ffxivbis.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,12 +9,9 @@ package me.arcanis.ffxivbis import akka.actor.typed.ActorSystem -import com.typesafe.config.ConfigFactory object ffxivbis { - def main(args: Array[String]): Unit = { - val config = ConfigFactory.load() - ActorSystem[Nothing](Application(), "ffxivbis", config) - } + def main(args: Array[String]): Unit = + ActorSystem[Nothing](Application(), "ffxivbis", Configuration.load()) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala index 356887d..971b869 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/Authorization.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,22 +8,18 @@ */ package me.arcanis.ffxivbis.http -import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.{ActorRef, Scheduler} import akka.http.scaladsl.model.headers._ import akka.http.scaladsl.server.AuthenticationFailedRejection._ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{GetUser, Message} -import me.arcanis.ffxivbis.models.Permission +import me.arcanis.ffxivbis.models.{Permission, User} import scala.concurrent.{ExecutionContext, Future} // idea comes from https://synkre.com/bcrypt-for-akka-http-password-encryption/ trait Authorization { - def storage: ActorRef[Message] + def auth: AuthorizationProvider def authenticateBasicBCrypt[T](realm: String, authenticate: (String, String) => Future[Option[T]]): Directive1[T] = { def challenge = HttpChallenges.basic(realm) @@ -38,34 +34,26 @@ trait Authorization { } } - def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = - storage.ask(GetUser(partyId, username, _)).map { - case Some(user) if user.verify(password) && user.verityScope(scope) => Some(username) - case _ => None - } - def authAdmin(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.admin, partyId)(username, password) def authGet(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.get, partyId)(username, password) def authPost(partyId: String)(username: String, password: String)(implicit - executionContext: ExecutionContext, - timeout: Timeout, - scheduler: Scheduler - ): Future[Option[String]] = + executionContext: ExecutionContext + ): Future[Option[User]] = authenticator(Permission.post, partyId)(username, password) + + private def authenticator(scope: Permission.Value, partyId: String)(username: String, password: String)(implicit + executionContext: ExecutionContext + ): Future[Option[User]] = + auth.get(partyId, username).map { + case Some(user) if user.verify(password) && user.verityScope(scope) => Some(user) + case _ => None + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala b/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala new file mode 100644 index 0000000..46cf1ff --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/AuthorizationProvider.scala @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2022 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 + +import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} +import akka.util.Timeout +import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} +import com.typesafe.config.Config +import me.arcanis.ffxivbis.messages.{GetUser, Message} +import me.arcanis.ffxivbis.models.User + +import java.util.concurrent.TimeUnit +import scala.concurrent.Future + +trait AuthorizationProvider { + + def get(partyId: String, username: String): Future[Option[User]] +} + +object AuthorizationProvider { + + def apply(config: Config, storage: ActorRef[Message], timeout: Timeout, scheduler: Scheduler): AuthorizationProvider = + new AuthorizationProvider { + private val cacheSize = config.getInt("me.arcanis.ffxivbis.web.authorization-cache.cache-size") + private val cacheTimeout = + config.getDuration("me.arcanis.ffxivbis.web.authorization-cache.cache-timeout", TimeUnit.MILLISECONDS) + + private val cache: LoadingCache[(String, String), Future[Option[User]]] = CacheBuilder + .newBuilder() + .expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS) + .maximumSize(cacheSize) + .build( + new CacheLoader[(String, String), Future[Option[User]]] { + override def load(key: (String, String)): Future[Option[User]] = { + val (partyId, username) = key + storage.ask(GetUser(partyId, username, _))(timeout, scheduler) + } + } + ) + + override def get(partyId: String, username: String): Future[Option[User]] = + cache.get((partyId, username)) + } +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala b/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala new file mode 100644 index 0000000..0e8b4b4 --- /dev/null +++ b/src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2022 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 + +import akka.http.scaladsl.model.headers.{`User-Agent`, Authorization, BasicHttpCredentials, Referer} +import akka.http.scaladsl.server.Directive0 +import akka.http.scaladsl.server.Directives.{extractClientIP, extractRequestContext, mapResponse, optionalHeaderValueByType} +import com.typesafe.scalalogging.Logger + +import java.time.{Instant, ZoneId} +import java.time.format.DateTimeFormatter +import java.util.Locale + +trait HttpLog { + + private val httpLogger = Logger("http") + + def withHttpLog: Directive0 = + extractRequestContext.flatMap { context => + val request = s"${context.request.method.name()} ${context.request.uri.path}" + + extractClientIP.flatMap { maybeRemoteAddr => + val remoteAddr = maybeRemoteAddr.toIP.getOrElse("-") + + optionalHeaderValueByType(Referer).flatMap { maybeReferer => + val referer = maybeReferer.map(_.uri).getOrElse("-") + + optionalHeaderValueByType(`User-Agent`).flatMap { maybeUserAgent => + val userAgent = maybeUserAgent.map(_.products.map(_.toString()).mkString(" ")).getOrElse("-") + + optionalHeaderValueByType(Authorization).flatMap { maybeAuth => + val remoteUser = maybeAuth + .map(_.credentials) + .collect { case BasicHttpCredentials(username, _) => + username + } + .getOrElse("-") + + val start = Instant.now.toEpochMilli + val timeLocal = HttpLog.httpLogDatetimeFormatter.format(Instant.now) + + mapResponse { response => + val time = (Instant.now.toEpochMilli - start) / 1000.0 + + val status = response.status.intValue() + val bytesSent = response.entity.getContentLengthOption.getAsLong + + httpLogger.debug( + s"""$remoteAddr - $remoteUser [$timeLocal] "$request" $status $bytesSent "$referer" "$userAgent" $time""" + ) + response + } + } + } + } + } + } + +} + +object HttpLog { + + val httpLogDatetimeFormatter: DateTimeFormatter = + DateTimeFormatter + .ofPattern("dd/MMM/uuuu:HH:mm:ss xx ") + .withLocale(Locale.UK) + .withZone(ZoneId.systemDefault()) +} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala index fdc0b76..a81d910 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/RootEndpoint.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,44 +8,30 @@ */ package me.arcanis.ffxivbis.http -import java.time.Instant - import akka.actor.typed.{ActorRef, ActorSystem, Scheduler} import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import akka.util.Timeout -import com.typesafe.scalalogging.{Logger, StrictLogging} +import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage]) - extends StrictLogging { + extends StrictLogging + with HttpLog { import me.arcanis.ffxivbis.utils.Implicits._ private val config = system.settings.config implicit val scheduler: Scheduler = system.scheduler - implicit val timeout: Timeout = - config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") + implicit val timeout: Timeout = config.getDuration("me.arcanis.ffxivbis.settings.request-timeout") - private val rootApiV1Endpoint: RootApiV1Endpoint = new RootApiV1Endpoint(storage, provider, config) - private val rootView: RootView = new RootView(storage, provider) - private val swagger: Swagger = new Swagger(config) - private val httpLogger = Logger("http") + private val auth = AuthorizationProvider(config, storage, timeout, scheduler) - private val withHttpLog: Directive0 = - extractRequestContext.flatMap { context => - val start = Instant.now.toEpochMilli - mapResponse { response => - val time = (Instant.now.toEpochMilli - start) / 1000.0 - httpLogger.debug( - s"""- - [${Instant.now}] "${context.request.method.name()} ${context.request.uri.path}" ${response.status - .intValue()} ${response.entity.getContentLengthOption.getAsLong} $time""" - ) - response - } - } + private val rootApiV1Endpoint = new RootApiV1Endpoint(storage, auth, provider, config) + private val rootView = new RootView(auth) + private val swagger = new Swagger(config) def route: Route = withHttpLog { @@ -68,7 +54,7 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro } ~ rootView.route private def swaggerUIRoute: Route = - path("swagger") { - getFromResource("html/swagger.html") + path("api-docs") { + getFromResource("html/redoc.html") } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala b/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala index 0c6dc4c..c4da73e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/Swagger.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). 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 723c3c1..24f7b28 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,14 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} +import me.arcanis.ffxivbis.http.helpers.BiSHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class BiSEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit +class BiSEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends BiSHelper @@ -49,29 +54,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider requestBody = new RequestBody( description = "player best in slot description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PlayerBiSLinkModel]))) ), responses = Array( new ApiResponse(responseCode = "201", description = "Best in slot set has been created"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -82,11 +87,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => put { - entity(as[PlayerBiSLinkResponse]) { bisLink => + entity(as[PlayerBiSLinkModel]) { bisLink => val playerId = bisLink.playerId.withPartyId(partyId) - onComplete(putBiS(playerId, bisLink.link)) { - case Success(_) => complete(StatusCodes.Created, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(putBiS(playerId, bisLink.link)) { + complete(StatusCodes.Created, HttpEntity.Empty) } } } @@ -116,24 +120,24 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider description = "Best in slot", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])) ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -146,9 +150,8 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(bis(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(bis(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) } } } @@ -169,29 +172,29 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider requestBody = new RequestBody( description = "action and piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Best in slot set has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -202,11 +205,10 @@ class BiSEndpoint(override val storage: ActorRef[Message], override val provider extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PieceActionResponse]) { action => + entity(as[PieceActionModel]) { action => 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 + onSuccess(doModifyBiS(action.action, playerId, action.piece.toPiece)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala index 6a85c11..dc4b2b1 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/HttpHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -19,18 +19,18 @@ trait HttpHandler extends StrictLogging { this: JsonSupport => def exceptionHandler: ExceptionHandler = ExceptionHandler { case ex: IllegalArgumentException => - complete(StatusCodes.BadRequest, ErrorResponse(ex.getMessage)) + complete(StatusCodes.BadRequest, ErrorModel(ex.getMessage)) case other: Exception => logger.error("exception during request completion", other) - complete(StatusCodes.InternalServerError, ErrorResponse("unknown server error")) + complete(StatusCodes.InternalServerError, ErrorModel("unknown server error")) } def rejectionHandler: RejectionHandler = RejectionHandler.default .mapRejectionResponse { case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => - val message = ErrorResponse(entity.data.utf8String).toJson + val message = ErrorModel(entity.data.utf8String).toJson response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint)) case other => other } 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 f5f695a..62a7c03 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,20 +21,23 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} +import me.arcanis.ffxivbis.http.helpers.LootHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper +class LootEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit + timeout: Timeout, + scheduler: Scheduler +) extends LootHelper with Authorization with JsonSupport with HttpHandler { - def route: Route = getLoot ~ modifyLoot + def route: Route = getLoot ~ modifyLoot ~ suggestLoot @GET @Path("party/{partyId}/loot") @@ -58,24 +61,24 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Loot list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])) + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])) ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -88,9 +91,8 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(loot(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(loot(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) } } } @@ -110,29 +112,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "action and piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Loot list has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -143,11 +145,10 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PieceActionResponse]) { action => + entity(as[PieceActionModel]) { action => val playerId = action.playerId.withPartyId(partyId) - onComplete(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(doModifyLoot(action.action, playerId, action.piece.toPiece, action.isFreeLoot)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -168,7 +169,7 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "piece description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PieceResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PieceModel]))) ), responses = Array( new ApiResponse( @@ -176,29 +177,29 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Players with counters ordered by priority to get this item", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])), ) ) ), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -209,10 +210,9 @@ class LootEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => put { - entity(as[PieceResponse]) { piece => - onComplete(suggestPiece(partyId, piece.toPiece)) { - case Success(response) => complete(response.map(PlayerIdWithCountersResponse.fromPlayerId)) - case Failure(exception) => throw exception + entity(as[PieceModel]) { piece => + onSuccess(suggestPiece(partyId, piece.toPiece)) { response => + complete(response.map(PlayerIdWithCountersModel.fromPlayerId)) } } } 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 3174882..2ce7dff 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,14 +21,18 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} +import me.arcanis.ffxivbis.http.helpers.PlayerHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import scala.util.{Failure, Success} @Path("/api/v1") -class PartyEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit +class PartyEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends PlayerHelper @@ -51,22 +55,22 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid new ApiResponse( responseCode = "200", description = "Party description", - content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -77,9 +81,8 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => get { - onComplete(getPartyDescription(partyId)) { - case Success(response) => complete(PartyDescriptionResponse.fromDescription(response)) - case Failure(exception) => throw exception + onSuccess(getPartyDescription(partyId)) { response => + complete(PartyDescriptionModel.fromDescription(response)) } } } @@ -98,29 +101,29 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid requestBody = new RequestBody( description = "new party description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PartyDescriptionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Party description has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -131,11 +134,10 @@ class PartyEndpoint(override val storage: ActorRef[Message], override val provid extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => post { - entity(as[PartyDescriptionResponse]) { partyDescription => + entity(as[PartyDescriptionModel]) { partyDescription => val description = partyDescription.copy(partyId = partyId) - onComplete(updateDescription(description.toDescription)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(updateDescription(description.toDescription)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } 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 5b1d930..5f0e449 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,15 +21,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} +import me.arcanis.ffxivbis.http.helpers.PlayerHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} import me.arcanis.ffxivbis.models.PlayerId import scala.util.{Failure, Success} @Path("/api/v1") -class PlayerEndpoint(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit +class PlayerEndpoint( + override val storage: ActorRef[Message], + override val provider: ActorRef[BiSProviderMessage], + override val auth: AuthorizationProvider +)(implicit timeout: Timeout, scheduler: Scheduler ) extends PlayerHelper @@ -37,7 +41,7 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi with JsonSupport with HttpHandler { - def route: Route = getParty ~ modifyParty + def route: Route = getParty ~ getPartyStats ~ modifyParty @GET @Path("party/{partyId}") @@ -61,24 +65,24 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi description = "Players list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerModel])), ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), @@ -91,9 +95,69 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi get { parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => val playerId = PlayerId(partyId, maybeNick, maybeJob) - onComplete(getPlayers(partyId, playerId)) { - case Success(response) => complete(response.map(PlayerResponse.fromPlayer)) - case Failure(exception) => throw exception + onSuccess(getPlayers(partyId, playerId)) { response => + complete(response.map(PlayerModel.fromPlayer)) + } + } + } + } + } + } + + @GET + @Path("party/{partyId}/stats") + @Produces(value = Array("application/json")) + @Operation( + 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 = "nick", + in = ParameterIn.QUERY, + description = "player nick name to filter", + example = "Siuan Sanche" + ), + new Parameter(name = "job", in = ParameterIn.QUERY, description = "player job to filter", example = "DNC"), + ), + responses = Array( + new ApiResponse( + responseCode = "200", + description = "Party loot statistics", + content = Array( + new Content( + array = new ArraySchema(schema = new Schema(implementation = classOf[PlayerIdWithCountersModel])), + ) + ) + ), + new ApiResponse( + responseCode = "401", + description = "Supplied authorization is invalid", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "403", + description = "Access is forbidden", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "500", + description = "Internal server error", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + ), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), + tags = Array("party"), + ) + def getPartyStats: Route = + path("party" / Segment / "stats") { partyId => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => + get { + parameters("nick".as[String].?, "job".as[String].?) { (maybeNick, maybeJob) => + val playerId = PlayerId(partyId, maybeNick, maybeJob) + onSuccess(getPlayers(partyId, playerId)) { response => + complete(response.map(player => PlayerIdWithCountersModel.fromPlayerId(player.withCounters(None)))) } } } @@ -113,29 +177,29 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi requestBody = new RequestBody( description = "player description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[PlayerActionModel]))) ), responses = Array( new ApiResponse(responseCode = "202", description = "Party has been modified"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), @@ -145,11 +209,12 @@ class PlayerEndpoint(override val storage: ActorRef[Message], override val provi path("party" / Segment) { partyId => extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - entity(as[PlayerActionResponse]) { action => - 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 + post { + entity(as[PlayerActionModel]) { action => + val player = action.playerId.toPlayer.copy(partyId = partyId) + onSuccess(doModifyPlayer(action.action, player)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) + } } } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala index a33c547..0fecf00 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/RootApiV1Endpoint.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -13,21 +13,27 @@ import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route import akka.util.Timeout import com.typesafe.config.Config +import me.arcanis.ffxivbis.http.AuthorizationProvider import me.arcanis.ffxivbis.http.api.v1.json.JsonSupport import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -class RootApiV1Endpoint(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage], config: Config)(implicit +class RootApiV1Endpoint( + storage: ActorRef[Message], + auth: AuthorizationProvider, + provider: ActorRef[BiSProviderMessage], + config: Config +)(implicit timeout: Timeout, scheduler: Scheduler ) extends JsonSupport with HttpHandler { - private val biSEndpoint = new BiSEndpoint(storage, provider) - private val lootEndpoint = new LootEndpoint(storage) - private val partyEndpoint = new PartyEndpoint(storage, provider) - private val playerEndpoint = new PlayerEndpoint(storage, provider) + private val biSEndpoint = new BiSEndpoint(storage, provider, auth) + private val lootEndpoint = new LootEndpoint(storage, auth) + private val partyEndpoint = new PartyEndpoint(storage, provider, auth) + private val playerEndpoint = new PlayerEndpoint(storage, provider, auth) private val typesEndpoint = new TypesEndpoint(config) - private val userEndpoint = new UserEndpoint(storage) + private val userEndpoint = new UserEndpoint(storage, auth) def route: Route = handleExceptions(exceptionHandler) { diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala index 1df51c2..a1dc408 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpoint.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,17 +11,48 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server._ import com.typesafe.config.Config +import io.swagger.v3.oas.annotations.Operation 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 jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} +import me.arcanis.ffxivbis.models._ @Path("/api/v1") class TypesEndpoint(config: Config) extends JsonSupport { - def route: Route = getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority + def route: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority + + @GET + @Path("types/jobs/all") + @Produces(value = Array("application/json")) + @Operation( + summary = "full jobs list", + description = "Returns the available jobs including any job", + responses = Array( + new ApiResponse( + responseCode = "200", + description = "List of available jobs with AnyJob", + 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[ErrorModel]))) + ), + ), + tags = Array("types"), + ) + def getAllJobs: Route = + path("types" / "jobs" / "all") { + get { + complete(Job.availableWithAnyJob.map(_.toString)) + } + } @GET @Path("types/jobs") @@ -42,7 +73,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -50,7 +81,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { def getJobs: Route = path("types" / "jobs") { get { - complete(Job.availableWithAnyJob.map(_.toString)) + complete(Job.available.map(_.toString)) } } @@ -73,7 +104,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -104,7 +135,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -135,7 +166,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), @@ -166,7 +197,7 @@ class TypesEndpoint(config: Config) extends JsonSupport { new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("types"), 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 9726566..e2d9232 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,19 +21,22 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement import io.swagger.v3.oas.annotations.{Operation, Parameter} import jakarta.ws.rs._ import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.http.{Authorization, UserHelper} +import me.arcanis.ffxivbis.http.helpers.UserHelper +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} import me.arcanis.ffxivbis.messages.Message import me.arcanis.ffxivbis.models.Permission import scala.util.{Failure, Success} @Path("/api/v1") -class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends UserHelper +class UserEndpoint(override val storage: ActorRef[Message], override val auth: AuthorizationProvider)(implicit + timeout: Timeout, + scheduler: Scheduler +) extends UserHelper with Authorization with JsonSupport { - def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers + def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent @PUT @Path("party") @@ -44,24 +47,28 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "party administrator description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) ), responses = Array( - new ApiResponse(responseCode = "200", description = "Party has been created"), + new ApiResponse( + responseCode = "200", + description = "Party has been created", + content = Array(new Content(schema = new Schema(implementation = classOf[PartyIdModel]))) + ), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "406", description = "Party with the specified ID already exists", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), tags = Array("party"), @@ -70,15 +77,12 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti path("party") { extractExecutionContext { implicit executionContext => put { - entity(as[UserResponse]) { user => - onComplete(newPartyId) { - case Success(partyId) => - val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) - onComplete(addUser(admin, isHashedPassword = false)) { - case Success(_) => complete(PartyIdResponse(partyId)) - case Failure(exception) => throw exception - } - case Failure(exception) => throw exception + entity(as[UserModel]) { user => + onSuccess(newPartyId) { partyId => + val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) + onSuccess(addUser(admin, isHashedPassword = false)) { + complete(PartyIdModel(partyId)) + } } } } @@ -97,29 +101,29 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti requestBody = new RequestBody( description = "user description", required = true, - content = Array(new Content(schema = new Schema(implementation = classOf[UserResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) ), responses = Array( new ApiResponse(responseCode = "201", description = "User has been created"), new ApiResponse( responseCode = "400", description = "Invalid parameters were supplied", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), @@ -130,11 +134,10 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => post { - entity(as[UserResponse]) { user => + entity(as[UserModel]) { user => val withPartyId = user.toUser.copy(partyId = partyId) - onComplete(addUser(withPartyId, isHashedPassword = false)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(addUser(withPartyId, isHashedPassword = false)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -156,17 +159,17 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), @@ -177,9 +180,8 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => delete { - onComplete(removeUser(partyId, username)) { - case Success(_) => complete(StatusCodes.Accepted, HttpEntity.Empty) - case Failure(exception) => throw exception + onSuccess(removeUser(partyId, username)) { + complete(StatusCodes.Accepted, HttpEntity.Empty) } } } @@ -201,27 +203,27 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti description = "Users list", content = Array( new Content( - array = new ArraySchema(schema = new Schema(implementation = classOf[UserResponse])), + array = new ArraySchema(schema = new Schema(implementation = classOf[UserModel])), ) ) ), new ApiResponse( responseCode = "401", description = "Supplied authorization is invalid", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "403", description = "Access is forbidden", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), new ApiResponse( responseCode = "500", description = "Internal server error", - content = Array(new Content(schema = new Schema(implementation = classOf[ErrorResponse]))) + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) ), ), - security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), tags = Array("users"), ) def getUsers: Route = @@ -229,12 +231,56 @@ class UserEndpoint(override val storage: ActorRef[Message])(implicit timeout: Ti extractExecutionContext { implicit executionContext => authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => get { - onComplete(users(partyId)) { - case Success(response) => complete(response.map(UserResponse.fromUser)) - case Failure(exception) => throw exception + onSuccess(users(partyId)) { response => + complete(response.map(UserModel.fromUser)) } } } } } + + @GET + @Path("party/{partyId}/users/current") + @Produces(value = Array("application/json")) + @Operation( + 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"), + ), + responses = Array( + new ApiResponse( + responseCode = "200", + description = "User descriptor", + content = Array(new Content(schema = new Schema(implementation = classOf[UserModel]))) + ), + new ApiResponse( + responseCode = "401", + description = "Supplied authorization is invalid", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "403", + description = "Access is forbidden", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + new ApiResponse( + responseCode = "500", + description = "Internal server error", + content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) + ), + ), + security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("admin"))), + tags = Array("users"), + ) + def getUsersCurrent: Route = + path("party" / Segment / "users" / "current") { partyId => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + get { + complete(UserModel.fromUser(user)) + } + } + } + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala index 3a8d3af..6051720 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ApiAction.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala similarity index 65% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala index 81bfcff..8de54cb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PartyIdResponse(@Schema(description = "party id", required = true) partyId: String) +case class ErrorModel(@Schema(description = "error message", required = true) message: String) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala index 4486244..85e6bbc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/JsonSupport.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,12 +8,12 @@ */ package me.arcanis.ffxivbis.http.api.v1.json -import java.time.Instant - import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import me.arcanis.ffxivbis.models.Permission import spray.json._ +import java.time.Instant + trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { private def enumFormat[E <: Enumeration](enum: E): RootJsonFormat[E#Value] = @@ -38,19 +38,19 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { implicit val actionFormat: RootJsonFormat[ApiAction.Value] = enumFormat(ApiAction) implicit val permissionFormat: RootJsonFormat[Permission.Value] = enumFormat(Permission) - implicit val errorFormat: RootJsonFormat[ErrorResponse] = jsonFormat1(ErrorResponse.apply) - implicit val partyIdFormat: RootJsonFormat[PartyIdResponse] = jsonFormat1(PartyIdResponse.apply) - implicit val pieceFormat: RootJsonFormat[PieceResponse] = jsonFormat3(PieceResponse.apply) - implicit val lootFormat: RootJsonFormat[LootResponse] = jsonFormat3(LootResponse.apply) - implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionResponse] = jsonFormat2( - PartyDescriptionResponse.apply + implicit val errorFormat: RootJsonFormat[ErrorModel] = jsonFormat1(ErrorModel.apply) + implicit val partyIdFormat: RootJsonFormat[PartyIdModel] = jsonFormat1(PartyIdModel.apply) + implicit val pieceFormat: RootJsonFormat[PieceModel] = jsonFormat3(PieceModel.apply) + implicit val lootFormat: RootJsonFormat[LootModel] = jsonFormat3(LootModel.apply) + implicit val partyDescriptionFormat: RootJsonFormat[PartyDescriptionModel] = jsonFormat2( + PartyDescriptionModel.apply ) - implicit val playerFormat: RootJsonFormat[PlayerResponse] = jsonFormat7(PlayerResponse.apply) - implicit val playerActionFormat: RootJsonFormat[PlayerActionResponse] = jsonFormat2(PlayerActionResponse.apply) - implicit val playerIdFormat: RootJsonFormat[PlayerIdResponse] = jsonFormat3(PlayerIdResponse.apply) - implicit val pieceActionFormat: RootJsonFormat[PieceActionResponse] = jsonFormat4(PieceActionResponse.apply) - implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkResponse] = jsonFormat2(PlayerBiSLinkResponse.apply) - implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersResponse] = - jsonFormat9(PlayerIdWithCountersResponse.apply) - implicit val userFormat: RootJsonFormat[UserResponse] = jsonFormat4(UserResponse.apply) + implicit val playerFormat: RootJsonFormat[PlayerModel] = jsonFormat9(PlayerModel.apply) + implicit val playerActionFormat: RootJsonFormat[PlayerActionModel] = jsonFormat2(PlayerActionModel.apply) + implicit val playerIdFormat: RootJsonFormat[PlayerIdModel] = jsonFormat3(PlayerIdModel.apply) + implicit val pieceActionFormat: RootJsonFormat[PieceActionModel] = jsonFormat4(PieceActionModel.apply) + implicit val playerBiSLinkFormat: RootJsonFormat[PlayerBiSLinkModel] = jsonFormat2(PlayerBiSLinkModel.apply) + implicit val playerIdWithCountersFormat: RootJsonFormat[PlayerIdWithCountersModel] = + jsonFormat9(PlayerIdWithCountersModel.apply) + implicit val userFormat: RootJsonFormat[UserModel] = jsonFormat4(UserModel.apply) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala similarity index 54% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala index 0bf0d42..8f72c85 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/LootModel.scala @@ -1,12 +1,20 @@ +/* + * Copyright (c) 2019-2022 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.json -import java.time.Instant - import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.Loot -case class LootResponse( - @Schema(description = "looted piece", required = true) piece: PieceResponse, +import java.time.Instant + +case class LootModel( + @Schema(description = "looted piece", required = true) piece: PieceModel, @Schema(description = "loot timestamp", required = true) timestamp: Instant, @Schema(description = "is loot free for all", required = true) isFreeLoot: Boolean ) { @@ -14,8 +22,8 @@ case class LootResponse( def toLoot: Loot = Loot(-1, piece.toPiece, timestamp, isFreeLoot) } -object LootResponse { +object LootModel { - def fromLoot(loot: Loot): LootResponse = - LootResponse(PieceResponse.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) + def fromLoot(loot: Loot): LootModel = + LootModel(PieceModel.fromPiece(loot.piece), loot.timestamp, loot.isFreeLoot) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala similarity index 65% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala index 7111c14..bfb5b2b 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyDescriptionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,16 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PartyDescription -case class PartyDescriptionResponse( - @Schema(description = "party id", required = true) partyId: String, +case class PartyDescriptionModel( + @Schema(description = "party id", required = true, example = "abcdefgh") partyId: String, @Schema(description = "party name") partyAlias: Option[String] ) { def toDescription: PartyDescription = PartyDescription(partyId, partyAlias) } -object PartyDescriptionResponse { +object PartyDescriptionModel { - def fromDescription(description: PartyDescription): PartyDescriptionResponse = - PartyDescriptionResponse(description.partyId, description.partyAlias) + def fromDescription(description: PartyDescription): PartyDescriptionModel = + PartyDescriptionModel(description.partyId, description.partyAlias) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala similarity index 62% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala index ba3e7a0..88fc028 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/ErrorResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PartyIdModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,4 +10,4 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class ErrorResponse(@Schema(description = "error message", required = true) message: String) +case class PartyIdModel(@Schema(description = "party id", required = true, example = "abcdefgh") partyId: String) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala similarity index 71% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala index b90026c..0487f22 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceActionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,14 +10,14 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PieceActionResponse( +case class PieceActionModel( @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) playerId: PlayerIdResponse, - @Schema(description = "is piece free to roll or not") isFreeLoot: Option[Boolean] + @Schema(description = "piece description", required = true) piece: PieceModel, + @Schema(description = "player description", required = true) playerId: PlayerIdModel, + @Schema(description = "is piece free to roll or not", `type` = "boolean") isFreeLoot: Option[Boolean] ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala similarity index 67% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala index 9a7e172..b67cb5a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PieceModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,8 +11,8 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, Piece, PieceType} -case class PieceResponse( - @Schema(description = "piece type", required = true) pieceType: String, +case class PieceModel( + @Schema(description = "piece type", required = true, example = "Savage") pieceType: String, @Schema(description = "job name to which piece belong or AnyJob", required = true, example = "DNC") job: String, @Schema(description = "piece name", required = true, example = "body") piece: String ) { @@ -20,8 +20,8 @@ case class PieceResponse( def toPiece: Piece = Piece(piece, PieceType.withName(pieceType), Job.withName(job)) } -object PieceResponse { +object PieceModel { - def fromPiece(piece: Piece): PieceResponse = - PieceResponse(piece.pieceType.toString, piece.job.toString, piece.piece) + def fromPiece(piece: Piece): PieceModel = + PieceModel(piece.pieceType.toString, piece.job.toString, piece.piece) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala similarity index 84% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala index f79ca93..7b240ff 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerActionModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,7 +10,7 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PlayerActionResponse( +case class PlayerActionModel( @Schema( description = "action to perform", required = true, @@ -18,5 +18,5 @@ case class PlayerActionResponse( allowableValues = Array("add", "remove"), example = "add" ) action: ApiAction.Value, - @Schema(description = "player description", required = true) playerId: PlayerResponse + @Schema(description = "player description", required = true) playerId: PlayerModel ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala similarity index 82% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala index b4625ff..7dfc244 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerBiSLinkModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -10,11 +10,11 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema -case class PlayerBiSLinkResponse( +case class PlayerBiSLinkModel( @Schema( description = "link to player best in slot", required = true, example = "https://ffxiv.ariyala.com/19V5R" ) link: String, - @Schema(description = "player description", required = true) playerId: PlayerIdResponse + @Schema(description = "player description", required = true) playerId: PlayerIdModel ) diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala similarity index 75% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala index 2b54925..efca7e6 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Job, PlayerId} -case class PlayerIdResponse( +case class PlayerIdModel( @Schema(description = "unique party ID. Required in responses", example = "abcdefgh") 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 @@ -21,8 +21,8 @@ case class PlayerIdResponse( PlayerId(partyId, Job.withName(job), nick) } -object PlayerIdResponse { +object PlayerIdModel { - def fromPlayerId(playerId: PlayerId): PlayerIdResponse = - PlayerIdResponse(Some(playerId.partyId), playerId.job.toString, playerId.nick) + def fromPlayerId(playerId: PlayerId): PlayerIdModel = + PlayerIdModel(Some(playerId.partyId), playerId.job.toString, playerId.nick) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala similarity index 84% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala index 8485102..df72789 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerIdWithCountersModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,22 +11,22 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.PlayerIdWithCounters -case class PlayerIdWithCountersResponse( +case class PlayerIdWithCountersModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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, @Schema(description = "player loot priority", required = true) priority: Int, @Schema(description = "count of savage pieces in best in slot", required = true) bisCountTotal: Int, - @Schema(description = "count of looted pieces", required = true) lootCount: Int, + @Schema(description = "count of looted pieces of this type", required = true) lootCount: Int, @Schema(description = "count of looted pieces which are parts of best in slot", required = true) lootCountBiS: Int, @Schema(description = "total count of looted pieces", required = true) lootCountTotal: Int ) -object PlayerIdWithCountersResponse { +object PlayerIdWithCountersModel { - def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersResponse = - PlayerIdWithCountersResponse( + def fromPlayerId(playerIdWithCounters: PlayerIdWithCounters): PlayerIdWithCountersModel = + PlayerIdWithCountersModel( playerIdWithCounters.partyId, playerIdWithCounters.job.toString, playerIdWithCounters.nick, diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala similarity index 63% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala index 2a4b987..62457fb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/PlayerModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,14 +11,16 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{BiS, Job, Player} -case class PlayerResponse( +case class PlayerModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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[PieceResponse]], - @Schema(description = "looted pieces") loot: Option[Seq[LootResponse]], + @Schema(description = "pieces in best in slot") bis: Option[Seq[PieceModel]], + @Schema(description = "looted pieces") loot: Option[Seq[LootModel]], @Schema(description = "link to best in slot", example = "https://ffxiv.ariyala.com/19V5R") link: Option[String], - @Schema(description = "player loot priority", `type` = "number") priority: Option[Int] + @Schema(description = "player loot priority", `type` = "number") priority: Option[Int], + @Schema(description = "count of looted pieces which are parts of best in slot") lootCountBiS: Option[Int], + @Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int], ) { def toPlayer: Player = @@ -34,16 +36,18 @@ case class PlayerResponse( ) } -object PlayerResponse { +object PlayerModel { - def fromPlayer(player: Player): PlayerResponse = - PlayerResponse( + def fromPlayer(player: Player): PlayerModel = + PlayerModel( player.partyId, player.job.toString, player.nick, - Some(player.bis.pieces.map(PieceResponse.fromPiece)), - Some(player.loot.map(LootResponse.fromLoot)), + Some(player.bis.pieces.map(PieceModel.fromPiece)), + Some(player.loot.map(LootModel.fromLoot)), player.link, - Some(player.priority) + Some(player.priority), + Some(player.lootCountBiS), + Some(player.lootCountTotal), ) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala similarity index 80% rename from src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala rename to src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala index 6786140..437d940 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserResponse.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/api/v1/json/UserModel.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,13 +11,14 @@ package me.arcanis.ffxivbis.http.api.v1.json import io.swagger.v3.oas.annotations.media.Schema import me.arcanis.ffxivbis.models.{Permission, User} -case class UserResponse( +case class UserModel( @Schema(description = "unique party ID", required = true, example = "abcdefgh") 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 = "user permission", defaultValue = "get", + `type` = "string", allowableValues = Array("get", "post", "admin") ) permission: Option[Permission.Value] = None ) { @@ -26,8 +27,8 @@ case class UserResponse( User(partyId, username, password, permission.getOrElse(Permission.get)) } -object UserResponse { +object UserModel { - def fromUser(user: User): UserResponse = - UserResponse(user.partyId, user.username, "", Some(user.permission)) + def fromUser(user: User): UserModel = + UserModel(user.partyId, user.username, "", Some(user.permission)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala similarity index 82% rename from src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala index f1a5fce..5089dbe 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/BiSHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BiSHelper.scala @@ -1,18 +1,10 @@ -/* - * 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 +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceToBis, GetBiS, Message, RemovePieceFromBiS, RemovePiecesFromBiS} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala similarity index 67% rename from src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala index 3ddc56e..2a28b0a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/BisProviderHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/BisProviderHelper.scala @@ -1,15 +1,7 @@ -/* - * 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 +package me.arcanis.ffxivbis.http.helpers -import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} import me.arcanis.ffxivbis.models.{BiS, Job} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala similarity index 83% rename from src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala index 57b1351..ad65a72 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/LootHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/LootHelper.scala @@ -1,18 +1,10 @@ -/* - * 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 +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceTo, GetLoot, Message, RemovePieceFrom, SuggestLoot} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{Piece, Player, PlayerId, PlayerIdWithCounters} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala similarity index 85% rename from src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala index f10e7b2..80a3647 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/PlayerHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/PlayerHelper.scala @@ -1,18 +1,10 @@ -/* - * 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 +package me.arcanis.ffxivbis.http.helpers -import akka.actor.typed.{ActorRef, Scheduler} import akka.actor.typed.scaladsl.AskPattern.Askable +import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout import me.arcanis.ffxivbis.http.api.v1.json.ApiAction -import me.arcanis.ffxivbis.messages.{AddPieceToBis, AddPlayer, GetParty, GetPartyDescription, GetPlayer, Message, RemovePlayer, UpdateParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{PartyDescription, Player, PlayerId} import scala.concurrent.{ExecutionContext, Future} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala similarity index 74% rename from src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala rename to src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala index dc6c202..a2b6be0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/UserHelper.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/helpers/UserHelper.scala @@ -1,17 +1,9 @@ -/* - * 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 +package me.arcanis.ffxivbis.http.helpers import akka.actor.typed.scaladsl.AskPattern.Askable import akka.actor.typed.{ActorRef, Scheduler} import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{AddUser, DeleteUser, GetNewPartyId, GetUser, GetUsers, Message} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.User import scala.concurrent.Future diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala deleted file mode 100644 index 5a9d049..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BasePartyView.scala +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, PlayerHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} - -import scala.util.{Failure, Success} - -class BasePartyView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])( - implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with Authorization { - - def route: Route = getIndex - - def getIndex: Route = - path("party" / Segment) { partyId => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - onComplete(getPartyDescription(partyId)) { - case Success(description) => - complete(StatusCodes.OK, RootView.toHtml(BasePartyView.template(partyId, description.alias))) - case Failure(exception) => throw exception - } - } - } - } - } -} - -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") - - def template(partyId: String, alias: String): String = - "" + - html( - lang := "en", - head( - titleTag(s"Party $alias"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2(s"Party $alias"), - br, - h2(a(href := s"/party/$partyId/players", title := "party")("party")), - h2(a(href := s"/party/$partyId/bis", title := "bis management")("best in slot")), - h2(a(href := s"/party/$partyId/loot", title := "loot management")("loot")), - h2(a(href := s"/party/$partyId/suggest", title := "suggest loot")("suggest")), - hr, - h2(a(href := s"/party/$partyId/users", title := "user management")("users")) - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala deleted file mode 100644 index 57a5c42..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/BiSView.scala +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, BiSHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class BiSView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends BiSHelper - with Authorization { - - def route: Route = getBiS ~ modifyBiS - - def getBiS: Route = - path("party" / Segment / "bis") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - bis(partyId, None) - .map { players => - BiSView.template(partyId, players, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyBiS: Route = - path("party" / Segment / "bis") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "player".as[String], - "piece".as[String].?, - "piece_type".as[String].?, - "link".as[String].?, - "action".as[String] - ) { (player, maybePiece, maybePieceType, maybeLink, action) => - onComplete(modifyBiSCall(partyId, player, maybePiece, maybePieceType, maybeLink, action)) { _ => - redirect(s"/party/$partyId/bis", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyBiSCall( - partyId: String, - player: String, - maybePiece: Option[String], - maybePieceType: Option[String], - maybeLink: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def getPiece(playerId: PlayerId, piece: String, pieceType: String) = - Try(Piece(piece, PieceType.withName(pieceType), playerId.job)).toOption - - def bisAction(playerId: PlayerId, piece: String, pieceType: String)(fn: Piece => Future[Unit]) = - getPiece(playerId, piece, pieceType) match { - case Some(item) => fn(item) - case _ => Future.failed(new Error(s"Could not construct piece from `$piece ($pieceType)`")) - } - - PlayerId(partyId, player) match { - case Some(playerId) => - (maybePiece, maybePieceType, action, maybeLink.map(_.trim).filter(_.nonEmpty)) match { - case (Some(piece), Some(pieceType), "add", _) => - bisAction(playerId, piece, pieceType)(addPieceBiS(playerId, _)) - case (Some(piece), Some(pieceType), "remove", _) => - bisAction(playerId, piece, pieceType)(removePieceBiS(playerId, _)) - case (_, _, "create", Some(link)) => putBiS(playerId, link) - case _ => Future.failed(new Error(s"Could not perform $action")) - } - case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) - } - } -} - -object BiSView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[Player], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Best in slot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Best in slot"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/bis", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - form(action := s"/party/$partyId/bis", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - input(name := "link", id := "link", placeholder := "player bis link", title := "link", `type` := "text"), - input(name := "action", id := "action", `type` := "hidden", value := "create"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("player"), - th("piece"), - th("piece type"), - th("") - ), - for (player <- party; piece <- player.bis.pieces) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(`class` := "include_search")(piece.piece), - td(piece.pieceType.toString), - td( - form(action := s"/party/$partyId/bis", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input(name := "piece", id := "piece", `type` := "hidden", value := piece.piece), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := piece.pieceType.toString - ), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala deleted file mode 100644 index 9a35e81..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/ErrorView.scala +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.view - -import scalatags.Text -import scalatags.Text.all._ - -object ErrorView { - - def template(error: Option[String]): Text.TypedTag[String] = error match { - case Some(text) => p(id := "error", s"Error occurs: $text") - case None => p("") - } -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala deleted file mode 100644 index 0b76606..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/ExportToCSVView.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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.view - -import scalatags.Text -import scalatags.Text.all._ - -object ExportToCSVView { - - def template: Text.TypedTag[String] = - div( - button(onclick := "exportTableToCsv('result.csv')")("Export to CSV"), - script(src := "/static/table_export.js", `type` := "text/javascript") - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala deleted file mode 100644 index 97d772f..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/IndexView.scala +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{PlayerHelper, UserHelper} -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models.{PartyDescription, Permission, User} - -import scala.concurrent.Future -import scala.util.{Failure, Success} - -class IndexView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with UserHelper { - - def route: Route = createParty ~ getIndex - - def createParty: Route = - path("party") { - extractExecutionContext { implicit executionContext => - post { - formFields("username".as[String], "password".as[String], "alias".as[String].?) { - (username, password, maybeAlias) => - onComplete { - newPartyId.flatMap { partyId => - val user = User(partyId, username, password, Permission.admin) - addUser(user, isHashedPassword = false).flatMap { _ => - if (maybeAlias.getOrElse("").isEmpty) Future.successful(partyId) - else updateDescription(PartyDescription(partyId, maybeAlias)).map(_ => partyId) - } - } - } { - case Success(partyId) => redirect(s"/party/$partyId", StatusCodes.Found) - case Failure(exception) => throw exception - } - } - } - } - } - - def getIndex: Route = - pathEndOrSingleSlash { - get { - parameters("partyId".as[String].?) { - case Some(partyId) => redirect(s"/party/$partyId", StatusCodes.Found) - case _ => complete(StatusCodes.OK, RootView.toHtml(IndexView.template)) - } - } - } -} - -object IndexView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template: String = - "" + - html( - head( - titleTag("FFXIV loot helper"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - form(action := s"party", method := "post")( - label("create a new party"), - input(name := "alias", id := "alias", placeholder := "party alias", title := "alias", `type` := "text"), - input( - name := "username", - id := "username", - placeholder := "username", - title := "username", - `type` := "text" - ), - input( - name := "password", - id := "password", - placeholder := "password", - title := "password", - `type` := "password" - ), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - br, - form(action := "/", method := "get")( - label("already have party?"), - input(name := "partyId", id := "partyId", placeholder := "party id", title := "party id", `type` := "text"), - input(name := "go", id := "go", `type` := "submit", value := "go") - ) - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala deleted file mode 100644 index a15d023..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootSuggestView.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Job, Piece, PieceType, PlayerIdWithCounters} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -class LootSuggestView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper - with Authorization { - - def route: Route = getIndex ~ suggestLoot - - def getIndex: Route = - path("party" / Segment / "suggest") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - val text = LootSuggestView.template(partyId, Seq.empty, None, false, None) - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - - def suggestLoot: Route = - path("party" / Segment / "suggest") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - post { - formFields("piece".as[String], "job".as[String], "piece_type".as[String], "free_loot".as[String].?) { - (piece, job, pieceType, maybeFreeLoot) => - import me.arcanis.ffxivbis.utils.Implicits._ - - val maybePiece = Try(Piece(piece, PieceType.withName(pieceType), Job.withName(job))).toOption - - onComplete(suggestLootCall(partyId, maybePiece)) { - case Success(players) => - val text = LootSuggestView.template(partyId, players, maybePiece, maybeFreeLoot, None) - complete(StatusCodes.OK, RootView.toHtml(text)) - case Failure(exception) => - val text = LootSuggestView.template(partyId, Seq.empty, None, false, Some(exception.getMessage)) - complete(StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - private def suggestLootCall(partyId: String, maybePiece: Option[Piece])(implicit - executionContext: ExecutionContext, - timeout: Timeout - ): Future[Seq[PlayerIdWithCounters]] = - maybePiece match { - case Some(piece) => suggestPiece(partyId, piece) - case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece`")) - } -} - -object LootSuggestView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template( - partyId: String, - party: Seq[PlayerIdWithCounters], - piece: Option[Piece], - isFreeLoot: Boolean, - error: Option[String] - ): String = - "" + - html( - lang := "en", - head( - titleTag("Suggest loot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Suggest loot"), - for (part <- piece) yield p(s"Piece ${part.piece} (${part.pieceType})"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/suggest", method := "post")( - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "job", id := "job", title := "job")( - for (job <- Job.availableWithAnyJob) yield option(job.toString) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"), - label(`for` := "free_loot")("is free loot"), - input(name := "suggest", id := "suggest", `type` := "submit", value := "suggest") - ), - table(id := "result")( - tr( - th("player"), - th("is required"), - th("these pieces looted"), - th("total bis pieces looted"), - th("total pieces looted"), - th("") - ), - for (player <- party) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(player.isRequiredToString), - td(player.lootCount), - td(player.lootCountBiS), - td(player.lootCountTotal), - td( - form(action := s"/party/$partyId/loot", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input( - name := "piece", - id := "piece", - `type` := "hidden", - value := piece.map(_.piece).getOrElse("") - ), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := piece.map(_.pieceType.toString).getOrElse("") - ), - input( - name := "free_loot", - id := "free_loot", - `type` := "hidden", - value := (if (isFreeLoot) "yes" else "no") - ), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala deleted file mode 100644 index a4d6ef2..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/LootView.scala +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, LootHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Piece, PieceType, Player, PlayerId} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class LootView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends LootHelper - with Authorization { - - def route: Route = getLoot ~ modifyLoot - - def getLoot: Route = - path("party" / Segment / "loot") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - loot(partyId, None) - .map { players => - LootView.template(partyId, players, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyLoot: Route = - path("party" / Segment / "loot") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "player".as[String], - "piece".as[String], - "piece_type".as[String], - "action".as[String], - "free_loot".as[String].? - ) { (player, piece, pieceType, action, isFreeLoot) => - onComplete(modifyLootCall(partyId, player, piece, pieceType, isFreeLoot, action)) { _ => - redirect(s"/party/$partyId/loot", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyLootCall( - partyId: String, - player: String, - maybePiece: String, - maybePieceType: String, - maybeFreeLoot: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - import me.arcanis.ffxivbis.utils.Implicits._ - - def getPiece(playerId: PlayerId) = - Try(Piece(maybePiece, PieceType.withName(maybePieceType), playerId.job)).toOption - - PlayerId(partyId, player) match { - case Some(playerId) => - (getPiece(playerId), action) match { - case (Some(piece), "add") => addPieceLoot(playerId, piece, maybeFreeLoot) - case (Some(piece), "remove") => removePieceLoot(playerId, piece) - case _ => Future.failed(new Error(s"Could not construct piece from `$maybePiece ($maybePieceType)`")) - } - case _ => Future.failed(new Error(s"Could not construct player id from `$player`")) - } - } -} - -object LootView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[Player], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Loot"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Loot"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/loot", method := "post")( - select(name := "player", id := "player", title := "player")( - for (player <- party) yield option(player.playerId.toString) - ), - select(name := "piece", id := "piece", title := "piece")( - for (piece <- Piece.available) yield option(piece) - ), - select(name := "piece_type", id := "piece_type", title := "piece type")( - for (pieceType <- PieceType.available) yield option(pieceType.toString) - ), - input(name := "free_loot", id := "free_loot", title := "is free loot", `type` := "checkbox"), - label(`for` := "free_loot")("is free loot"), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("player"), - th("piece"), - th("piece type"), - th("is free loot"), - th("timestamp"), - th("") - ), - for (player <- party; loot <- player.loot) - yield tr( - td(`class` := "include_search")(player.playerId.toString), - td(`class` := "include_search")(loot.piece.piece), - td(loot.piece.pieceType.toString), - td(loot.isFreeLootToString), - td(loot.timestamp.toString), - td( - form(action := s"/party/$partyId/loot", method := "post")( - input(name := "player", id := "player", `type` := "hidden", value := player.playerId.toString), - input(name := "piece", id := "piece", `type` := "hidden", value := loot.piece.piece), - input( - name := "piece_type", - id := "piece_type", - `type` := "hidden", - value := loot.piece.pieceType.toString - ), - input(name := "free_loot", id := "free_loot", `type` := "hidden", value := loot.isFreeLootToString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) - -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala deleted file mode 100644 index 25c850d..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/PlayerView.scala +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -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.messages.{BiSProviderMessage, Message} -import me.arcanis.ffxivbis.models._ - -import scala.concurrent.{ExecutionContext, Future} - -class PlayerView(override val storage: ActorRef[Message], override val provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) extends PlayerHelper - with Authorization { - - def route: Route = getParty ~ modifyParty - - def getParty: Route = - path("party" / Segment / "players") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { _ => - get { - complete { - getPlayers(partyId, None) - .map { players => - PlayerView.template(partyId, players.map(_.withCounters(None)), None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyParty: Route = - path("party" / Segment / "players") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authPost(partyId)) { _ => - post { - formFields( - "nick".as[String], - "job".as[String], - "priority".as[Int].?, - "link".as[String].?, - "action".as[String] - ) { (nick, job, maybePriority, maybeLink, action) => - onComplete(modifyPartyCall(partyId, nick, job, maybePriority, maybeLink, action)) { _ => - redirect(s"/party/$partyId/players", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyPartyCall( - partyId: String, - nick: String, - job: String, - maybePriority: Option[Int], - maybeLink: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def maybePlayerId = PlayerId(partyId, Some(nick), Some(job)) - def player(playerId: PlayerId) = - Player(-1, partyId, playerId.job, playerId.nick, BiS.empty, Seq.empty, maybeLink, maybePriority.getOrElse(0)) - - (action, maybePlayerId) match { - case ("add", Some(playerId)) => addPlayer(player(playerId)) - case ("remove", Some(playerId)) => removePlayer(playerId) - case _ => Future.failed(new Error(s"Could not perform $action with $nick ($job)")) - } - } -} - -object PlayerView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, party: Seq[PlayerIdWithCounters], error: Option[String]): String = - "" + - html( - lang := "en", - head( - titleTag("Party"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Party"), - ErrorView.template(error), - SearchLineView.template, - 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.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"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("nick"), - th("job"), - th("total bis pieces looted"), - th("total pieces looted"), - th("priority"), - th("") - ), - for (player <- party) - yield tr( - td(`class` := "include_search")(player.nick), - td(`class` := "include_search")(player.job.toString), - td(player.lootCountBiS), - td(player.lootCountTotal), - td(player.priority), - td( - form(action := s"/party/$partyId/players", method := "post")( - input(name := "nick", id := "nick", `type` := "hidden", value := player.nick), - input(name := "job", id := "job", `type` := "hidden", value := player.job.toString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala index 62e3fed..afc3bca 100644 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala +++ b/src/main/scala/me/arcanis/ffxivbis/http/view/RootView.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,34 +8,73 @@ */ package me.arcanis.ffxivbis.http.view -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.{ContentTypes, HttpEntity} +import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.messages.{BiSProviderMessage, Message} +import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider} -class RootView(storage: ActorRef[Message], provider: ActorRef[BiSProviderMessage])(implicit - timeout: Timeout, - scheduler: Scheduler -) { +class RootView(override val auth: AuthorizationProvider) extends Authorization { - private val basePartyView = new BasePartyView(storage, provider) - private val indexView = new IndexView(storage, provider) + def route: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers - private val biSView = new BiSView(storage, provider) - private val lootView = new LootView(storage) - private val lootSuggestView = new LootSuggestView(storage) - private val playerView = new PlayerView(storage, provider) - private val userView = new UserView(storage) + def getBiS: Route = + path("party" / Segment / "bis") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/bis.html") + } + } + } + } - def route: Route = - basePartyView.route ~ indexView.route ~ - biSView.route ~ lootView.route ~ lootSuggestView.route ~ playerView.route ~ userView.route -} - -object RootView { - - def toHtml(template: String): HttpEntity.Strict = - HttpEntity(ContentTypes.`text/html(UTF-8)`, template) + def getIndex: Route = + pathEndOrSingleSlash { + getFromResource("html/index.html") + } + + def getLoot: Route = + path("party" / Segment / "loot") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/loot.html") + } + } + } + } + + def getParty: Route = + path("party" / Segment) { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authGet(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/party.html") + } + } + } + } + + def getUsers: Route = + path("party" / Segment / "users") { partyId: String => + extractExecutionContext { implicit executionContext => + authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { user => + respondWithHeaders( + RawHeader("X-Party-Id", partyId), + RawHeader("X-User-Permission", user.permission.toString) + ) { + getFromResource("html/users.html") + } + } + } + } } diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala deleted file mode 100644 index ef2274d..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/SearchLineView.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.view - -import scalatags.Text -import scalatags.Text.all._ - -object SearchLineView { - - def template: Text.TypedTag[String] = - div( - input( - `type` := "text", - id := "search", - onkeyup := "searchTable()", - placeholder := "search for data", - title := "search" - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala b/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala deleted file mode 100644 index 68c7536..0000000 --- a/src/main/scala/me/arcanis/ffxivbis/http/view/UserView.scala +++ /dev/null @@ -1,149 +0,0 @@ -/* - * 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.view - -import akka.actor.typed.{ActorRef, Scheduler} -import akka.http.scaladsl.model.StatusCodes -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import akka.util.Timeout -import me.arcanis.ffxivbis.http.{Authorization, UserHelper} -import me.arcanis.ffxivbis.messages.Message -import me.arcanis.ffxivbis.models.{Permission, User} - -import scala.concurrent.{ExecutionContext, Future} -import scala.util.Try - -class UserView(override val storage: ActorRef[Message])(implicit timeout: Timeout, scheduler: Scheduler) - extends UserHelper - with Authorization { - - def route: Route = getUsers ~ modifyUsers - - def getUsers: Route = - path("party" / Segment / "users") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => - get { - complete { - users(partyId) - .map { users => - UserView.template(partyId, users, None) - } - .map { text => - (StatusCodes.OK, RootView.toHtml(text)) - } - } - } - } - } - } - - def modifyUsers: Route = - path("party" / Segment / "users") { partyId: String => - extractExecutionContext { implicit executionContext => - authenticateBasicBCrypt(s"party $partyId", authAdmin(partyId)) { _ => - post { - formFields("username".as[String], "password".as[String].?, "permission".as[String].?, "action".as[String]) { - (username, maybePassword, maybePermission, action) => - onComplete(modifyUsersCall(partyId, username, maybePassword, maybePermission, action)) { case _ => - redirect(s"/party/$partyId/users", StatusCodes.Found) - } - } - } - } - } - } - - private def modifyUsersCall( - partyId: String, - username: String, - maybePassword: Option[String], - maybePermission: Option[String], - action: String - )(implicit executionContext: ExecutionContext, timeout: Timeout): Future[Unit] = { - def permission: Option[Permission.Value] = - maybePermission.flatMap(p => Try(Permission.withName(p)).toOption) - - action match { - case "add" => - (maybePassword, permission) match { - case (Some(password), Some(permission)) => - addUser(User(partyId, username, password, permission), isHashedPassword = false) - case _ => - Future.failed( - new Error(s"Could not construct permission/password from `$maybePermission`/`$maybePassword`") - ) - } - case "remove" => removeUser(partyId, username) - case _ => Future.failed(new Error(s"Could not perform $action")) - } - } -} - -object UserView { - import scalatags.Text.all._ - import scalatags.Text.tags2.{title => titleTag} - - def template(partyId: String, users: Seq[User], error: Option[String]) = - "" + - html( - lang := "en", - head( - titleTag("Users"), - link(rel := "stylesheet", `type` := "text/css", href := "/static/styles.css") - ), - body( - h2("Users"), - ErrorView.template(error), - SearchLineView.template, - form(action := s"/party/$partyId/users", method := "post")( - input( - name := "username", - id := "username", - placeholder := "username", - title := "username", - `type` := "text" - ), - input( - name := "password", - id := "password", - placeholder := "password", - title := "password", - `type` := "password" - ), - select(name := "permission", id := "permission", title := "permission")(option("get"), option("post")), - input(name := "action", id := "action", `type` := "hidden", value := "add"), - input(name := "add", id := "add", `type` := "submit", value := "add") - ), - table(id := "result")( - tr( - th("username"), - th("permission"), - th("") - ), - for (user <- users) - yield tr( - td(`class` := "include_search")(user.username), - td(user.permission.toString), - td( - form(action := s"/party/$partyId/users", method := "post")( - input(name := "username", id := "username", `type` := "hidden", value := user.username.toString), - input(name := "action", id := "action", `type` := "hidden", value := "remove"), - input(name := "remove", id := "remove", `type` := "submit", value := "x") - ) - ) - ) - ), - ExportToCSVView.template, - BasePartyView.root(partyId), - script(src := "/static/table_search.js", `type` := "text/javascript") - ) - ) -} diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala index daa5c48..703c9fc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/BiSProviderMessage.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 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.messages import akka.actor.typed.ActorRef diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala index 409d399..cdceda5 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/ContolMessage.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 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.messages import akka.actor.typed.ActorRef diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala index 07e16ed..72ff50e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/DatabaseMessage.scala @@ -1,7 +1,15 @@ +/* + * Copyright (c) 2019-2022 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.messages import akka.actor.typed.{ActorRef, Behavior} -import me.arcanis.ffxivbis.models.{Party, PartyDescription, Piece, Player, PlayerId, User} +import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.service.LootSelector sealed trait DatabaseMessage extends Message { @@ -15,65 +23,75 @@ object DatabaseMessage { } // bis handler -case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait BisDatabaseMessage extends DatabaseMessage + +case class AddPieceToBis(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } -case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage +case class GetBiS(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) + extends BisDatabaseMessage -case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePieceFromBiS(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } -case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePiecesFromBiS(playerId: PlayerId, replyTo: ActorRef[Unit]) extends BisDatabaseMessage { override def partyId: String = playerId.partyId } // loot handler +trait LootDatabaseMessage extends DatabaseMessage + case class AddPieceTo(playerId: PlayerId, piece: Piece, isFreeLoot: Boolean, replyTo: ActorRef[Unit]) - extends DatabaseMessage { + extends LootDatabaseMessage { override def partyId: String = playerId.partyId } -case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) extends DatabaseMessage +case class GetLoot(partyId: String, playerId: Option[PlayerId], replyTo: ActorRef[Seq[Player]]) + extends LootDatabaseMessage -case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePieceFrom(playerId: PlayerId, piece: Piece, replyTo: ActorRef[Unit]) extends LootDatabaseMessage { override def partyId: String = playerId.partyId } case class SuggestLoot(partyId: String, piece: Piece, replyTo: ActorRef[LootSelector.LootSelectorResult]) - extends DatabaseMessage + extends LootDatabaseMessage // party handler -case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait PartyDatabaseMessage extends DatabaseMessage + +case class AddPlayer(player: Player, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = player.partyId } -case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends DatabaseMessage +case class GetParty(partyId: String, replyTo: ActorRef[Party]) extends PartyDatabaseMessage -case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends DatabaseMessage +case class GetPartyDescription(partyId: String, replyTo: ActorRef[PartyDescription]) extends PartyDatabaseMessage -case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends DatabaseMessage { +case class GetPlayer(playerId: PlayerId, replyTo: ActorRef[Option[Player]]) extends PartyDatabaseMessage { override def partyId: String = playerId.partyId } -case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class RemovePlayer(playerId: PlayerId, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = playerId.partyId } -case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends DatabaseMessage { +case class UpdateParty(partyDescription: PartyDescription, replyTo: ActorRef[Unit]) extends PartyDatabaseMessage { override def partyId: String = partyDescription.partyId } // user handler -case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends DatabaseMessage { +trait UserDatabaseMessage extends DatabaseMessage + +case class AddUser(user: User, isHashedPassword: Boolean, replyTo: ActorRef[Unit]) extends UserDatabaseMessage { override def partyId: String = user.partyId } -case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends DatabaseMessage +case class DeleteUser(partyId: String, username: String, replyTo: ActorRef[Unit]) extends UserDatabaseMessage -case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends DatabaseMessage +case class Exists(partyId: String, replyTo: ActorRef[Boolean]) extends UserDatabaseMessage -case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends DatabaseMessage +case class GetUser(partyId: String, username: String, replyTo: ActorRef[Option[User]]) extends UserDatabaseMessage -case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends DatabaseMessage +case class GetUsers(partyId: String, replyTo: ActorRef[Seq[User]]) extends UserDatabaseMessage diff --git a/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala b/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala index def973d..fe392f2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala +++ b/src/main/scala/me/arcanis/ffxivbis/messages/Message.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 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.messages import akka.actor.typed.Behavior @@ -5,5 +13,6 @@ import akka.actor.typed.Behavior trait Message object Message { + type Handler = PartialFunction[Message, Behavior[Message]] } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala index 9fd5d64..7eb3e29 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/BiS.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -25,6 +25,7 @@ case class BiS(pieces: Seq[Piece]) { .withDefaultValue(0) def withPiece(piece: Piece): BiS = copy(pieces :+ piece) + def withoutPiece(piece: Piece): BiS = copy(pieces.filterNot(_.strictEqual(piece))) override def equals(obj: Any): Boolean = { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala index 2afb6fc..df4a4aa 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Job.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Job.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -29,6 +29,7 @@ object Job { sealed trait Job extends Equals { def leftSide: LeftSide + def rightSide: RightSide // conversion to string to avoid recursion diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala index 1f1af48..afcedd2 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Loot.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala index a2e5950..62a4330 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Party.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Party.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,10 +17,13 @@ import scala.util.Random case class Party(partyDescription: PartyDescription, rules: Seq[String], players: Map[PlayerId, Player]) extends StrictLogging { + require(players.keys.forall(_.partyId == partyDescription.partyId), "party id must be same") def getPlayers: Seq[Player] = players.values.toSeq + def player(playerId: PlayerId): Option[Player] = players.get(playerId) + def withPlayer(player: Player): Party = try { require(player.partyId == partyDescription.partyId, "player must belong to this party") diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala index 9da1f4e..a138856 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PartyDescription.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala index 7dd4603..87d3af0 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Piece.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -11,7 +11,9 @@ package me.arcanis.ffxivbis.models sealed trait Piece extends Equals { def pieceType: PieceType.PieceType + def job: Job.Job + def piece: String def withJob(other: Job.Job): Piece diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala index 88cf4b4..bb1f7ef 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PieceType.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 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.models object PieceType { diff --git a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala index 9821da1..6faeb31 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/Player.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/Player.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,10 +21,12 @@ case class Player( require(job ne Job.AnyJob, "AnyJob is not allowed") val playerId: PlayerId = PlayerId(partyId, job, nick) + def withBiS(set: Option[BiS]): Player = set match { case Some(value) => copy(bis = value) case None => this } + def withCounters(piece: Option[Piece]): PlayerIdWithCounters = PlayerIdWithCounters( partyId, @@ -32,12 +34,14 @@ case class Player( nick, isRequired(piece), priority, - bisCountTotal(piece), + bisCountTotal, lootCount(piece), - lootCountBiS(piece), - lootCountTotal(piece) + lootCountBiS, + lootCountTotal ) + def withLoot(piece: Loot): Player = withLoot(Seq(piece)) + def withLoot(list: Seq[Loot]): Player = { require(loot.forall(_.playerId == id), "player id must be same") copy(loot = loot ++ list) @@ -51,12 +55,16 @@ case class Player( case Some(_) => lootCount(piece) == 0 } - def bisCountTotal(piece: Option[Piece]): Int = bis.pieces.count(_.pieceType == PieceType.Savage) + def bisCountTotal: Int = bis.pieces.count(_.pieceType == PieceType.Savage) + def lootCount(piece: Option[Piece]): Int = piece match { case Some(p) => loot.count(item => !item.isFreeLoot && item.piece == p) - case None => lootCountTotal(piece) + case None => lootCountTotal } - def lootCountBiS(piece: Option[Piece]): Int = loot.map(_.piece).count(bis.hasPiece) - def lootCountTotal(piece: Option[Piece]): Int = loot.count(!_.isFreeLoot) - def lootPriority(piece: Piece): Int = priority + + def lootCountBiS: Int = loot.map(_.piece).count(bis.hasPiece) + + def lootCountTotal: Int = loot.count(!_.isFreeLoot) + + def lootPriority: Int = priority } diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala index 6fbdf68..d7d1821 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerId.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -14,6 +14,7 @@ import scala.util.matching.Regex trait PlayerIdBase { def job: Job.Job + def nick: String override def toString: String = s"$nick ($job)" diff --git a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala index 14703bb..ba1a1a6 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/PlayerIdWithCounters.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -23,7 +23,9 @@ case class PlayerIdWithCounters( def gt(that: PlayerIdWithCounters, orderBy: Seq[String]): Boolean = withCounters(orderBy) > that.withCounters(orderBy) + def isRequiredToString: String = if (isRequired) "yes" else "no" + def playerId: PlayerId = PlayerId(partyId, job, nick) private val counters: Map[String, Int] = Map( @@ -42,6 +44,7 @@ case class PlayerIdWithCounters( object PlayerIdWithCounters { private case class PlayerCountersComparator(values: Int*) { + def >(that: PlayerCountersComparator): Boolean = { @scala.annotation.tailrec def compareLists(left: List[Int], right: List[Int]): Boolean = diff --git a/src/main/scala/me/arcanis/ffxivbis/models/User.scala b/src/main/scala/me/arcanis/ffxivbis/models/User.scala index 759b5e2..17caacc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/models/User.scala +++ b/src/main/scala/me/arcanis/ffxivbis/models/User.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,7 +17,10 @@ object Permission extends Enumeration { case class User(partyId: String, username: String, password: String, permission: Permission.Value) { def hash: String = BCrypt.hashpw(password, BCrypt.gensalt) + def verify(plain: String): Boolean = BCrypt.checkpw(plain, password) + def verityScope(scope: Permission.Value): Boolean = permission >= scope + def withHashedPassword: User = copy(password = hash) } diff --git a/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala b/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala index e77be60..1943af8 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/LootSelector.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala index dbea45a..b11a43e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/PartyService.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,11 +9,11 @@ package me.arcanis.ffxivbis.service import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{ActorRef, Behavior, DispatcherSelector, Scheduler} import akka.util.Timeout import com.typesafe.scalalogging.StrictLogging -import me.arcanis.ffxivbis.messages.{DatabaseMessage, Exists, ForgetParty, GetNewPartyId, GetParty, Message, StoreParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.Party import scala.concurrent.duration.FiniteDuration diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala index edcfb55..d3e108e 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/BisProvider.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,10 +8,9 @@ */ package me.arcanis.ffxivbis.service.bis -import java.nio.file.Paths import akka.actor.ClassicActorSystemProvider -import akka.actor.typed.{Behavior, PostStop, Signal} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import akka.actor.typed.{Behavior, PostStop, Signal} import akka.http.scaladsl.model._ import com.typesafe.scalalogging.StrictLogging import me.arcanis.ffxivbis.messages.{BiSProviderMessage, DownloadBiS} @@ -20,6 +19,7 @@ import me.arcanis.ffxivbis.service.bis.parser.Parser import me.arcanis.ffxivbis.service.bis.parser.impl.{Ariyala, Etro} import spray.json._ +import java.nio.file.Paths import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success} diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala index d37dd35..4e224db 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/RequestExecutor.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala index bf8af9c..2db60cd 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/XivApi.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala index 3d2b192..fcf4481 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/Parser.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala index bbe2cda..eefae33 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Ariyala.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser.impl import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala index 304a879..e4f357d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/bis/parser/impl/Etro.scala @@ -1,3 +1,11 @@ +/* + * Copyright (c) 2019-2022 Evgeniy Alekseev. + * + * This file is part of ffxivbis + * (see https://github.com/arcan1s/ffxivbis). + * + * License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause + */ package me.arcanis.ffxivbis.service.bis.parser.impl import akka.http.scaladsl.model.Uri diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala index 89d81ba..45969cc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/Database.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala index fc37c51..ba12d3a 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseBiSHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPieceToBis, DatabaseMessage, GetBiS, RemovePieceFromBiS, RemovePiecesFromBiS} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.service.database.Database trait DatabaseBiSHandler { this: Database => diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala index 8d006c5..5fdefeb 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseImpl.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,8 +8,8 @@ */ package me.arcanis.ffxivbis.service.database.impl -import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} +import akka.actor.typed.{Behavior, DispatcherSelector} import com.typesafe.config.Config import me.arcanis.ffxivbis.messages.DatabaseMessage import me.arcanis.ffxivbis.service.database.Database diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala index 755b21d..3ae0b98 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseLootHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,12 +8,13 @@ */ package me.arcanis.ffxivbis.service.database.impl -import java.time.Instant import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPieceTo, DatabaseMessage, GetLoot, RemovePieceFrom, SuggestLoot} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.service.database.Database +import java.time.Instant + trait DatabaseLootHandler { this: Database => def lootHandler: DatabaseMessage.Handler = { 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 49b147f..fbbef7a 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddPlayer, DatabaseMessage, GetParty, GetPartyDescription, GetPlayer, RemovePlayer, UpdateParty} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.service.database.Database diff --git a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala index 5131034..d7a2a08 100644 --- a/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala +++ b/src/main/scala/me/arcanis/ffxivbis/service/database/impl/DatabaseUserHandler.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -9,7 +9,7 @@ package me.arcanis.ffxivbis.service.database.impl import akka.actor.typed.scaladsl.Behaviors -import me.arcanis.ffxivbis.messages.{AddUser, DatabaseMessage, DeleteUser, Exists, GetUser, GetUsers} +import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.service.database.Database trait DatabaseUserHandler { this: Database => diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala index e8f3e05..ffbfa4b 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/BiSProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,17 +8,17 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import slick.lifted.ForeignKeyQuery +import java.time.Instant import scala.concurrent.Future trait BiSProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class BiSRep(playerId: Long, created: Long, piece: String, pieceType: String, job: String) { + def toLoot: Loot = Loot( playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), @@ -26,7 +26,9 @@ trait BiSProfile { this: DatabaseProfile => isFreeLoot = false ) } + object BiSRep { + def fromPiece(playerId: Long, piece: Piece): BiSRep = BiSRep(playerId, DatabaseProfile.now, piece.piece, piece.pieceType.toString, piece.job.toString) } @@ -47,11 +49,15 @@ trait BiSProfile { this: DatabaseProfile => def deletePieceBiSById(piece: Piece)(playerId: Long): Future[Int] = db.run(pieceBiS(BiSRep.fromPiece(playerId, piece)).delete) + def deletePiecesBiSById(playerId: Long): Future[Int] = db.run(piecesBiS(Seq(playerId)).delete) + def getPiecesBiSById(playerId: Long): Future[Seq[Loot]] = getPiecesBiSById(Seq(playerId)) + def getPiecesBiSById(playerIds: Seq[Long]): Future[Seq[Loot]] = db.run(piecesBiS(playerIds).result).map(_.map(_.toLoot)) + def insertPieceBiSById(piece: Piece)(playerId: Long): Future[Int] = getPiecesBiSById(playerId).flatMap { case pieces if pieces.exists(loot => loot.piece.strictEqual(piece)) => Future.successful(0) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala index d287125..2ee01cc 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/DatabaseProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,13 +8,12 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import com.typesafe.config.Config import me.arcanis.ffxivbis.models.{Loot, Piece, PlayerId} import slick.basic.DatabaseConfig import slick.jdbc.JdbcProfile +import java.time.Instant import scala.concurrent.{ExecutionContext, Future} class DatabaseProfile(context: ExecutionContext, config: Config) @@ -40,12 +39,16 @@ class DatabaseProfile(context: ExecutionContext, config: Config) // generic bis api def deletePieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = byPlayerId(playerId, deletePieceBiSById(piece)) + def deletePiecesBiS(playerId: PlayerId): Future[Int] = byPlayerId(playerId, deletePiecesBiSById) + def getPiecesBiS(playerId: PlayerId): Future[Seq[Loot]] = byPlayerId(playerId, getPiecesBiSById) + def getPiecesBiS(partyId: String): Future[Seq[Loot]] = byPartyId(partyId, getPiecesBiSById) + def insertPieceBiS(playerId: PlayerId, piece: Piece): Future[Int] = byPlayerId(playerId, insertPieceBiSById(piece)) @@ -55,15 +58,19 @@ class DatabaseProfile(context: ExecutionContext, config: Config) val loot = Loot(-1, piece, Instant.now, isFreeLoot = false) byPlayerId(playerId, deletePieceById(loot)) } + def getPieces(playerId: PlayerId): Future[Seq[Loot]] = byPlayerId(playerId, getPiecesById) + def getPieces(partyId: String): Future[Seq[Loot]] = byPartyId(partyId, getPiecesById) + def insertPiece(playerId: PlayerId, loot: Loot): Future[Int] = byPlayerId(playerId, insertPieceById(loot)) private def byPartyId[T](partyId: String, callback: Seq[Long] => Future[T]): Future[T] = getPlayers(partyId).flatMap(callback) + private def byPlayerId[T](playerId: PlayerId, callback: Long => Future[T]): Future[T] = getPlayer(playerId).flatMap { case Some(id) => callback(id) @@ -74,6 +81,7 @@ class DatabaseProfile(context: ExecutionContext, config: Config) object DatabaseProfile { def now: Long = Instant.now.toEpochMilli + def getSection(config: Config): Config = { val section = config.getString("me.arcanis.ffxivbis.database.mode") config.getConfig("me.arcanis.ffxivbis.database").getConfig(section) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala index 32c3165..5b7d775 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/LootProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,11 +8,10 @@ */ package me.arcanis.ffxivbis.storage -import java.time.Instant - import me.arcanis.ffxivbis.models.{Job, Loot, Piece, PieceType} import slick.lifted.{ForeignKeyQuery, Index} +import java.time.Instant import scala.concurrent.Future trait LootProfile { this: DatabaseProfile => @@ -27,6 +26,7 @@ trait LootProfile { this: DatabaseProfile => job: String, isFreeLoot: Int ) { + def toLoot: Loot = Loot( playerId, Piece(piece, PieceType.withName(pieceType), Job.withName(job)), @@ -34,6 +34,7 @@ trait LootProfile { this: DatabaseProfile => isFreeLoot == 1 ) } + object LootRep { def fromLoot(playerId: Long, loot: Loot): LootRep = LootRep( @@ -70,14 +71,18 @@ trait LootProfile { this: DatabaseProfile => case Some(id) => db.run(lootTable.filter(_.lootId === id).delete) case _ => throw new IllegalArgumentException(s"Could not find piece $loot belong to $playerId") } + def getPiecesById(playerId: Long): Future[Seq[Loot]] = getPiecesById(Seq(playerId)) + def getPiecesById(playerIds: Seq[Long]): Future[Seq[Loot]] = db.run(piecesLoot(playerIds).result).map(_.map(_.toLoot)) + def insertPieceById(loot: Loot)(playerId: Long): Future[Int] = db.run(lootTable.insertOrUpdate(LootRep.fromLoot(playerId, loot))) private def pieceLoot(piece: LootRep) = piecesLoot(Seq(piece.playerId)).filter(_.piece === piece.piece) + private def piecesLoot(playerIds: Seq[Long]) = lootTable.filter(_.playerId.inSet(playerIds.toSet)) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala b/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala index c2ab08e..8c2b782 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/Migration.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -21,8 +21,8 @@ class Migration(config: Config) { val section = DatabaseProfile.getSection(config) val url = section.getString("db.url") - val username = section.getString("db.user") - val password = section.getString("db.password") + val username = Try(section.getString("db.user")).toOption.filter(_.nonEmpty).orNull + val password = Try(section.getString("db.password")).toOption.filter(_.nonEmpty).orNull val provider = url match { case s"jdbc:$p:$_" => p diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala index d79bf48..7c8ae75 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PartyProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -16,9 +16,12 @@ trait PartyProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class PartyRep(partyId: Option[Long], partyName: String, partyAlias: Option[String]) { + def toDescription: PartyDescription = PartyDescription(partyName, partyAlias) } + object PartyRep { + def fromDescription(party: PartyDescription, id: Option[Long]): PartyRep = PartyRep(id, party.partyId, party.partyAlias) } @@ -36,8 +39,10 @@ trait PartyProfile { this: DatabaseProfile => db.run( partyDescription(partyId).result.headOption.map(_.map(_.toDescription).getOrElse(PartyDescription.empty(partyId))) ) + def getUniquePartyId(partyId: String): Future[Option[Long]] = db.run(partyDescription(partyId).map(_.partyId).result.headOption) + def insertPartyDescription(partyDescription: PartyDescription): Future[Int] = getUniquePartyId(partyDescription.partyId).flatMap { case Some(id) => db.run(partiesTable.update(PartyRep.fromDescription(partyDescription, Some(id)))) diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala index 607a3f3..a660662 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/PlayersProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -24,10 +24,13 @@ trait PlayersProfile { this: DatabaseProfile => link: Option[String], priority: Int ) { + def toPlayer: Player = Player(playerId.getOrElse(-1), partyId, Job.withName(job), nick, BiS.empty, Seq.empty, link, priority) } + object PlayerRep { + def fromPlayer(player: Player, id: Option[Long]): PlayerRep = PlayerRep(player.partyId, id, DatabaseProfile.now, player.nick, player.job.toString, player.link, player.priority) } @@ -46,18 +49,23 @@ trait PlayersProfile { this: DatabaseProfile => } def deletePlayer(playerId: PlayerId): Future[Int] = db.run(player(playerId).delete) + def getParty(partyId: String): Future[Map[Long, Player]] = db.run(players(partyId).result) .map(_.foldLeft(Map.empty[Long, Player]) { case (acc, p @ PlayerRep(_, Some(id), _, _, _, _, _)) => acc + (id -> p.toPlayer) case (acc, _) => acc }) + def getPlayer(playerId: PlayerId): Future[Option[Long]] = db.run(player(playerId).map(_.playerId).result.headOption) + def getPlayerFull(playerId: PlayerId): Future[Option[Player]] = db.run(player(playerId).result.headOption.map(_.map(_.toPlayer))) + def getPlayers(partyId: String): Future[Seq[Long]] = db.run(players(partyId).map(_.playerId).result) + def insertPlayer(playerObj: Player): Future[Int] = getPlayer(playerObj.playerId).flatMap { case Some(id) => db.run(playersTable.update(PlayerRep.fromPlayer(playerObj, Some(id)))) @@ -69,6 +77,7 @@ trait PlayersProfile { this: DatabaseProfile => .filter(_.partyId === playerId.partyId) .filter(_.job === playerId.job.toString) .filter(_.nick === playerId.nick) + private def players(partyId: String) = playersTable.filter(_.partyId === partyId) } diff --git a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala index 7a5a09a..788775d 100644 --- a/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala +++ b/src/main/scala/me/arcanis/ffxivbis/storage/UsersProfile.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -17,9 +17,12 @@ trait UsersProfile { this: DatabaseProfile => import dbConfig.profile.api._ case class UserRep(partyId: String, userId: Option[Long], username: String, password: String, permission: String) { + def toUser: User = User(partyId, username, password, Permission.withName(permission)) } + object UserRep { + def fromUser(user: User, id: Option[Long]): UserRep = UserRep(user.partyId, id, user.username, user.password, user.permission.toString) } @@ -40,13 +43,21 @@ trait UsersProfile { this: DatabaseProfile => } def deleteUser(partyId: String, username: String): Future[Int] = - db.run(user(partyId, Some(username)).delete) + db.run( + user(partyId, Some(username)) + .filter(_.permission =!= Permission.admin.toString) // we do not allow to remove admins + .delete + ) + def exists(partyId: String): Future[Boolean] = db.run(user(partyId, None).exists.result) + def getUser(partyId: String, username: String): Future[Option[User]] = db.run(user(partyId, Some(username)).result.headOption).map(_.map(_.toUser)) + def getUsers(partyId: String): Future[Seq[User]] = db.run(user(partyId, None).result).map(_.map(_.toUser)) + def insertUser(userObj: User): Future[Int] = db.run(user(userObj.partyId, Some(userObj.username)).map(_.userId).result.headOption).flatMap { case Some(id) => db.run(usersTable.insertOrUpdate(UserRep.fromUser(userObj, Some(id)))) diff --git a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala index 0c01e61..e79b6ef 100644 --- a/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala +++ b/src/main/scala/me/arcanis/ffxivbis/utils/Implicits.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 Evgeniy Alekseev. + * Copyright (c) 2019-2022 Evgeniy Alekseev. * * This file is part of ffxivbis * (see https://github.com/arcan1s/ffxivbis). @@ -8,11 +8,10 @@ */ package me.arcanis.ffxivbis.utils -import java.time.Duration -import java.util.concurrent.TimeUnit - import akka.util.Timeout +import java.time.Duration +import java.util.concurrent.TimeUnit import scala.concurrent.duration.FiniteDuration import scala.language.implicitConversions diff --git a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala index 042e793..0ae0cad 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Fixtures.scala @@ -1,7 +1,10 @@ package me.arcanis.ffxivbis +import me.arcanis.ffxivbis.http.AuthorizationProvider import me.arcanis.ffxivbis.models._ +import scala.concurrent.Future + object Fixtures { lazy val bis: BiS = BiS( Seq( @@ -79,4 +82,6 @@ object Fixtures { lazy val userAdmin: User = User(partyId, "admin", userPassword, Permission.admin).withHashedPassword lazy val userGet: User = User(partyId, "get", userPassword, Permission.get).withHashedPassword lazy val users: Seq[User] = Seq(userAdmin, userGet) + + lazy val authProvider: AuthorizationProvider = (_: String, _: String) => Future.successful(Some(userAdmin)) } diff --git a/src/test/scala/me/arcanis/ffxivbis/Settings.scala b/src/test/scala/me/arcanis/ffxivbis/Settings.scala index e5c67f6..cea846e 100644 --- a/src/test/scala/me/arcanis/ffxivbis/Settings.scala +++ b/src/test/scala/me/arcanis/ffxivbis/Settings.scala @@ -1,9 +1,9 @@ package me.arcanis.ffxivbis -import java.io.File - import com.typesafe.config.{Config, ConfigFactory, ConfigValueFactory} +import java.io.File + object Settings { def config(values: Map[String, AnyRef]): Config = { @scala.annotation.tailrec diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala index 9ca5a3b..2238443 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/BiSEndpointTest.scala @@ -2,20 +2,20 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} import me.arcanis.ffxivbis.models.{BiS, Job} +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration import me.arcanis.ffxivbis.utils.Compare +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -31,14 +31,14 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT private val auth = Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) private val endpoint = Uri(s"/party/${Fixtures.partyId}/bis") - private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId) + private val playerId = PlayerIdModel.fromPlayerId(Fixtures.playerEmpty.playerId) private val askTimeout = 60 seconds implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout) private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new BiSEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -54,7 +54,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT super.afterAll() } - private def compareBiSResponse(actual: PlayerResponse, expected: PlayerResponse): Unit = { + private def compareBiSResponse(actual: PlayerModel, expected: PlayerModel): Unit = { actual.partyId shouldEqual expected.partyId actual.nick shouldEqual expected.nick actual.job shouldEqual expected.job @@ -66,7 +66,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "api v1 bis endpoint" must { "create best in slot set from ariyala" in { - val entity = PlayerBiSLinkResponse(Fixtures.link, playerId) + val entity = PlayerBiSLinkModel(Fixtures.link, playerId) Put(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Created @@ -76,19 +76,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "return best in slot set" in { val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "remove item from best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -97,19 +97,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val bis = BiS(Fixtures.bis.pieces.filterNot(_ == Fixtures.lootBody)) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "add item to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -117,19 +117,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "do not allow to add same item to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBody.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -137,19 +137,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "allow to add item with another type to best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -158,19 +158,19 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) val bis = Fixtures.bis.withPiece(piece.toPiece) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } } "remove only specific item from best in slot set" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -178,11 +178,11 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } @@ -190,15 +190,15 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT "totaly replace player bis" in { // add random item first - val piece = PieceResponse.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, None) + val piece = PieceModel.fromPiece(Fixtures.lootBodyCrafted.withJob(Job.DNC)) + val entity = PieceActionModel(ApiAction.add, piece, playerId, None) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted responseAs[String] shouldEqual "" } - val bisEntity = PlayerBiSLinkResponse(Fixtures.link, playerId) + val bisEntity = PlayerBiSLinkModel(Fixtures.link, playerId) Put(endpoint, bisEntity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Created @@ -206,11 +206,11 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = PlayerResponse.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) + val response = PlayerModel.fromPlayer(Fixtures.playerEmpty.withBiS(Some(Fixtures.bis))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val actual = responseAs[Seq[PlayerResponse]] + val actual = responseAs[Seq[PlayerModel]] actual.length shouldEqual 1 actual.foreach(compareBiSResponse(_, response)) } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala index 8a9e935..e45bf35 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/LootEndpointTest.scala @@ -1,23 +1,22 @@ package me.arcanis.ffxivbis.http.api.v1 -import java.time.Instant - import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} -import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.PartyService +import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike +import java.time.Instant import scala.concurrent.Await import scala.concurrent.duration._ import scala.language.postfixOps @@ -30,13 +29,13 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute private val auth = Authorization(BasicHttpCredentials(Fixtures.userAdmin.username, Fixtures.userPassword)) private val endpoint = Uri(s"/party/${Fixtures.partyId}/loot") - private val playerId = PlayerIdResponse.fromPlayerId(Fixtures.playerEmpty.playerId) + private val playerId = PlayerIdModel.fromPlayerId(Fixtures.playerEmpty.playerId) private val askTimeout = 60 seconds implicit private val routeTimeout: RouteTestTimeout = RouteTestTimeout(askTimeout) private val storage = testKit.spawn(Database()) private val party = testKit.spawn(PartyService(storage)) - private val route = new LootEndpoint(party)(askTimeout, testKit.scheduler).route + private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -55,8 +54,8 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "api v1 loot endpoint" must { "add item to loot" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.add, piece, playerId, Some(false)) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.add, piece, playerId, Some(false)) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -68,11 +67,11 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute import me.arcanis.ffxivbis.utils.Converters._ val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody))) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty.withLoot(Fixtures.lootBody))) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val withEmptyTimestamp = responseAs[Seq[PlayerResponse]].map { player => + val withEmptyTimestamp = responseAs[Seq[PlayerModel]].map { player => player.copy(loot = player.loot.map(_.map(_.copy(timestamp = Instant.ofEpochMilli(0))))) } withEmptyTimestamp shouldEqual response @@ -80,8 +79,8 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } "remove item from loot" in { - val piece = PieceResponse.fromPiece(Fixtures.lootBody) - val entity = PieceActionResponse(ApiAction.remove, piece, playerId, Some(false)) + val piece = PieceModel.fromPiece(Fixtures.lootBody) + val entity = PieceActionModel(ApiAction.remove, piece, playerId, Some(false)) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -89,11 +88,11 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute } val uri = endpoint.withQuery(Uri.Query(Map("nick" -> playerId.nick, "job" -> playerId.job))) - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty)) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty)) Get(uri).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + responseAs[Seq[PlayerModel]] shouldEqual response } } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala index 6cbb3d9..9b4aae1 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PartyEndpointTest.scala @@ -2,19 +2,19 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.AddUser import me.arcanis.ffxivbis.models.PartyDescription +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -36,7 +36,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new PartyEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -56,12 +56,12 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout "get empty party description" in { Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[PartyDescriptionResponse].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId) + responseAs[PartyDescriptionModel].toDescription shouldEqual PartyDescription.empty(Fixtures.partyId) } } "update party description" in { - val entity = PartyDescriptionResponse(Fixtures.partyId, Some("random party name")) + val entity = PartyDescriptionModel(Fixtures.partyId, Some("random party name")) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -69,7 +69,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[PartyDescriptionResponse].toDescription shouldEqual entity.toDescription + responseAs[PartyDescriptionModel].toDescription shouldEqual entity.toDescription } } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala index 3dd649f..cd1ccfd 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/PlayerEndpointTest.scala @@ -2,18 +2,18 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.messages.{AddPlayer, AddUser} +import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.bis.BisProvider import me.arcanis.ffxivbis.service.database.Database -import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -35,7 +35,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou private val storage = testKit.spawn(Database()) private val provider = testKit.spawn(BisProvider()) private val party = testKit.spawn(PartyService(storage)) - private val route = new PlayerEndpoint(party, provider)(askTimeout, testKit.scheduler).route + private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -54,11 +54,11 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou "api v1 player endpoint" must { "get users" in { - val response = Seq(PlayerResponse.fromPlayer(Fixtures.playerEmpty)) + val response = Seq(PlayerModel.fromPlayer(Fixtures.playerEmpty)) Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - responseAs[Seq[PlayerResponse]] shouldEqual response + responseAs[Seq[PlayerModel]] shouldEqual response } } diff --git a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala index 05a1720..84520aa 100644 --- a/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/http/api/v1/TypesEndpointTest.scala @@ -5,7 +5,7 @@ import akka.http.scaladsl.testkit.ScalatestRouteTest import com.typesafe.config.Config import me.arcanis.ffxivbis.Settings import me.arcanis.ffxivbis.http.api.v1.json._ -import me.arcanis.ffxivbis.models.{Job, Party, Permission, Piece, PieceType} +import me.arcanis.ffxivbis.models._ import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -22,6 +22,13 @@ class TypesEndpointTest extends AnyWordSpecLike "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 jobs WITH ANY JOB ALIAS" in { + Get("/types/jobs/all") ~> route ~> check { status shouldEqual StatusCodes.OK responseAs[Seq[String]] shouldEqual Job.availableWithAnyJob.map(_.toString) } 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 a965acf..26143e4 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 @@ -1,16 +1,16 @@ package me.arcanis.ffxivbis.http.api.v1 import akka.actor.testkit.typed.scaladsl.ActorTestKit -import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials} +import akka.http.scaladsl.model.{StatusCodes, Uri} import akka.http.scaladsl.testkit.{RouteTestTimeout, ScalatestRouteTest} import akka.testkit.TestKit import com.typesafe.config.Config -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.service.PartyService import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.storage.Migration +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike @@ -31,7 +31,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute private var partyId = Fixtures.partyId private val storage = testKit.spawn(Database()) private val party = testKit.spawn(PartyService(storage)) - private val route = new UserEndpoint(party)(askTimeout, testKit.scheduler).route + private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route override def beforeAll(): Unit = { super.beforeAll() @@ -49,16 +49,16 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute "create a party" in { val uri = Uri(s"/party") - val entity = UserResponse.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) + val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) Put(uri, entity) ~> route ~> check { status shouldEqual StatusCodes.OK - partyId = responseAs[PartyIdResponse].partyId + partyId = responseAs[PartyIdModel].partyId } } "add user" in { - val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) + val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId, password = Fixtures.userPassword2) Post(endpoint, entity).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -73,14 +73,24 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val users = responseAs[Seq[UserResponse]] + val users = responseAs[Seq[UserModel]] users.map(_.partyId).distinct shouldEqual Seq(partyId) users.map(user => user.username -> user.permission).toMap shouldEqual party } } + "get current user" in { + Get(Uri(s"${endpoint.path}/current")).withHeaders(auth) ~> route ~> check { + status shouldEqual StatusCodes.OK + + val user = responseAs[UserModel] + user.partyId shouldEqual Fixtures.partyId + user.username shouldEqual Fixtures.userAdmin.username + } + } + "remove user" in { - val entity = UserResponse.fromUser(Fixtures.userGet).copy(partyId = partyId) + val entity = UserModel.fromUser(Fixtures.userGet).copy(partyId = partyId) Delete(endpoint.toString + s"/${entity.username}").withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.Accepted @@ -92,7 +102,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute Get(endpoint).withHeaders(auth) ~> route ~> check { status shouldEqual StatusCodes.OK - val users = responseAs[Seq[UserResponse]] + val users = responseAs[Seq[UserModel]] users.map(_.partyId).distinct shouldEqual Seq(partyId) users.map(user => user.username -> user.permission).toMap shouldEqual party } diff --git a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala index 41e1aa1..fdeca76 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/LootSelectorTest.scala @@ -3,9 +3,9 @@ package me.arcanis.ffxivbis.service import akka.actor.testkit.typed.scaladsl.ActorTestKit import akka.actor.typed.scaladsl.AskPattern.Askable import me.arcanis.ffxivbis.messages.DownloadBiS -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.models._ import me.arcanis.ffxivbis.service.bis.BisProvider +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.BeforeAndAfterAll import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike diff --git a/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala b/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala index 654c034..6e25354 100644 --- a/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala +++ b/src/test/scala/me/arcanis/ffxivbis/service/bis/BisProviderTest.scala @@ -2,8 +2,8 @@ package me.arcanis.ffxivbis.service.bis import akka.actor.testkit.typed.scaladsl.ScalaTestWithActorTestKit import me.arcanis.ffxivbis.messages.DownloadBiS -import me.arcanis.ffxivbis.{Fixtures, Settings} import me.arcanis.ffxivbis.models._ +import me.arcanis.ffxivbis.{Fixtures, Settings} import org.scalatest.wordspec.AnyWordSpecLike import scala.concurrent.duration._ diff --git a/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala b/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala index 1747252..41ede69 100644 --- a/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala +++ b/src/test/scala/me/arcanis/ffxivbis/utils/Converters.scala @@ -1,9 +1,8 @@ package me.arcanis.ffxivbis.utils -import java.time.Instant - import me.arcanis.ffxivbis.models.{Loot, Piece} +import java.time.Instant import scala.language.implicitConversions object Converters {