13 Commits

38 changed files with 315 additions and 258 deletions

View File

@ -7,4 +7,3 @@ scalaVersion := "2.13.6"
scalacOptions ++= Seq("-deprecation", "-feature") scalacOptions ++= Seq("-deprecation", "-feature")
enablePlugins(JavaAppPackaging) enablePlugins(JavaAppPackaging)
coverageEnabled := true

View File

@ -12,6 +12,7 @@ libraryDependencies += "com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion libraryDependencies += "com.typesafe.akka" %% "akka-stream" % AkkaVersion
libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0" libraryDependencies += "com.github.swagger-akka-http" %% "swagger-akka-http" % "2.6.0"
libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0" libraryDependencies += "jakarta.platform" % "jakarta.jakartaee-web-api" % "9.1.0"
libraryDependencies += "ch.megard" %% "akka-http-cors" % "1.1.2"
libraryDependencies += "io.spray" %% "spray-json" % "1.3.6" libraryDependencies += "io.spray" %% "spray-json" % "1.3.6"

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper API</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Embed elements Elements via Web Component -->
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" type="text/css">
<link rel="shortcut icon" href="/static/favicon.ico">
</head>
<body>
<elements-api
apiDescriptionUrl="/api-docs/swagger.json"
router="hash"
layout="sidebar"
/>
</body>
</html>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon"> <link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="/static/styles.css" type="text/css">
</head> </head>
<body> <body>
@ -86,23 +86,24 @@
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade"> <div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<form class="modal-content"> <form class="modal-content" action="javascript:" onsubmit="updateBis()">
<div class="modal-header"> <div class="modal-header form-group row">
<div class="btn-group" role="group" aria-label="Update bis"> <div class="btn-group" role="group" aria-label="Update bis">
<input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked> <input id="add-piece-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off" checked>
<label class="btn btn-outline-primary" for="add-piece-btn">add piece</label> <label class="btn btn-outline-primary" for="add-piece-btn">add piece</label>
<input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off"> <input id="update-bis-btn" name="update-bis" type="radio" class="btn-check" autocomplete="off">
<label class="btn btn-outline-primary" for="update-bis-btn">update bis</label> <label class="btn btn-outline-primary" for="update-bis-btn">update bis</label>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div> </div>
</div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label> <label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player"></select> <select id="player" name="player" class="form-control" title="player" required></select>
</div> </div>
</div> </div>
<div id="piece-row" class="form-group row"> <div id="piece-row" class="form-group row">
@ -120,14 +121,14 @@
<div id="bis-link-row" class="form-group row" style="display: none"> <div id="bis-link-row" class="form-group row" style="display: none">
<label class="col-sm-4 col-form-label" for="bis-link">link</label> <label class="col-sm-4 col-form-label" for="bis-link">link</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="bis-link" name="link" class="form-control" placeholder="link to bis" onkeyup="disableSubmitBisButton()"> <input id="bis-link" name="link" class="form-control" placeholder="link to bis">
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-add-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPiece()" disabled>add</button> <button id="submit-add-bis-btn" type="submit" class="btn btn-primary">add</button>
<button id="submit-update-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="setBis()" style="display: none" disabled>set</button> <button id="submit-set-bis-btn" type="submit" class="btn btn-primary" style="display: none">set</button>
</div> </div>
</div> </div>
</form> </form>
@ -138,6 +139,7 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"> <ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li> <li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul> </ul>
<ul class="nav"> <ul class="nav">
@ -171,7 +173,7 @@
const updateButton = $("#update-btn"); const updateButton = $("#update-btn");
const submitAddBisButton = $("#submit-add-bis-btn"); const submitAddBisButton = $("#submit-add-bis-btn");
const submitUpdateBisButton = $("#submit-update-bis-btn"); const submitSetBisButton = $("#submit-set-bis-btn");
const updateBisDialog = $("#update-bis-dialog"); const updateBisDialog = $("#update-bis-dialog");
const addPieceButton = $("#add-piece-btn"); const addPieceButton = $("#add-piece-btn");
@ -208,12 +210,8 @@
success: function (_) { reload(); }, success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
} updateBisDialog.modal("hide");
return true; // action expects boolean result
function disableSubmitBisButton() {
const nonEmpty = (playerInput.val() !== null); // well lol
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
submitAddBisButton.attr("disabled", !(nonEmpty));
} }
function hideControls() { function hideControls() {
@ -222,20 +220,24 @@
} }
function hideLinkPart() { function hideLinkPart() {
disableSubmitBisButton();
bisLinkRow.hide(); bisLinkRow.hide();
submitUpdateBisButton.hide(); linkInput.prop("required", false);
submitSetBisButton.hide();
pieceRow.show(); pieceRow.show();
pieceTypeRow.show(); pieceTypeRow.show();
pieceInput.prop("required", true);
pieceTypeInput.prop("required", true);
submitAddBisButton.show(); submitAddBisButton.show();
} }
function hidePiecePart() { function hidePiecePart() {
disableSubmitBisButton();
bisLinkRow.show(); bisLinkRow.show();
submitUpdateBisButton.show(); linkInput.prop("required", true);
submitSetBisButton.show();
pieceRow.hide(); pieceRow.hide();
pieceTypeRow.hide(); pieceTypeRow.hide();
pieceInput.prop("required", false);
pieceTypeInput.prop("required", false);
submitAddBisButton.hide(); submitAddBisButton.hide();
} }
@ -269,7 +271,6 @@
return option; return option;
}); });
playerInput.empty().append(options); playerInput.empty().append(options);
disableSubmitBisButton();
}, },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
@ -327,6 +328,18 @@
success: function (_) { reload(); }, success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
updateBisDialog.modal("hide");
return true; // action expects boolean result
}
function updateBis() {
if (updateBisButton.is(":checked")) {
return setBis();
}
if (addPieceButton.is(":checked")) {
return addPiece();
}
return false; // should not happen
} }
$(function () { $(function () {
@ -344,6 +357,7 @@
table.bootstrapTable({}); table.bootstrapTable({});
reload(); reload();
reset();
}); });
</script> </script>

View File

@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon"> <link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="/static/styles.css" type="text/css">
</head> </head>
<body> <body>
@ -71,7 +71,9 @@
<div class="container"> <div class="container">
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"></ul> <ul class="nav">
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul>
<ul class="nav"> <ul class="nav">
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li> <li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
@ -122,7 +124,7 @@
password: passwordInput.val(), password: passwordInput.val(),
permission: "admin", permission: "admin",
}), }),
type: "PUT", type: "POST",
contentType: "application/json", contentType: "application/json",
dataType: "json", dataType: "json",
success: function (data) { success: function (data) {

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon"> <link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="/static/styles.css" type="text/css">
</head> </head>
<body> <body>
@ -88,35 +88,35 @@
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade"> <div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <form class="modal-content" action="javascript:" onsubmit="addLoot()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">add looted piece</h4> <h4 class="modal-title">add looted piece</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div> </div>
<form class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="player">player</label> <label class="col-sm-4 col-form-label" for="player">player</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="player" name="player" class="form-control" title="player"></select> <select id="player" name="player" class="form-control" title="player" required></select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece">piece</label> <label class="col-sm-4 col-form-label" for="piece">piece</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="piece" name="piece" class="form-control" title="piece"></select> <select id="piece" name="piece" class="form-control" title="piece" required></select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label> <label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="piece-type" name="pieceType" class="form-control" title="pieceType"></select> <select id="piece-type" name="pieceType" class="form-control" title="pieceType" required></select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">job</label> <label class="col-sm-4 col-form-label" for="job">job</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job"></select> <select id="job" name="job" class="form-control" title="job" required></select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -141,14 +141,14 @@
</tr> </tr>
</thead> </thead>
</table> </table>
</form> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button> <button type="button" class="btn btn-secondary" onclick="suggestLoot()">suggest</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button> <button type="submit" class="btn btn-primary">add</button>
</div>
</div> </div>
</form>
</div> </div>
</div> </div>
@ -156,6 +156,7 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"> <ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li> <li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul> </ul>
<ul class="nav"> <ul class="nav">
@ -189,7 +190,6 @@
const addButton = $("#add-btn"); const addButton = $("#add-btn");
const removeButton = $("#remove-btn"); const removeButton = $("#remove-btn");
const submitLootButton = $("#submit-btn");
const addLootDialog = $("#add-loot-dialog"); const addLootDialog = $("#add-loot-dialog");
const freeLootInput = $("#free-loot"); const freeLootInput = $("#free-loot");
@ -221,6 +221,8 @@
success: function (_) { reload(); }, success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
addLootDialog.modal("hide");
return true; // action expects boolean result
} }
function hideControls() { function hideControls() {
@ -260,7 +262,6 @@
return option; return option;
}); });
playerInput.empty().append(options); playerInput.empty().append(options);
submitLootButton.attr("disabled", options.length === 0);
}, },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
@ -310,7 +311,7 @@
return { return {
nick: stat.nick, nick: stat.nick,
job: stat.job, job: stat.job,
isRequired: stat.isRequired, isRequired: stat.isRequired ? "yes" : "no",
lootCount: stat.lootCount, lootCount: stat.lootCount,
lootCountBiS: stat.lootCountBiS, lootCountBiS: stat.lootCountBiS,
lootCountTotal: stat.lootCountTotal, lootCountTotal: stat.lootCountTotal,

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon"> <link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="/static/styles.css" type="text/css">
</head> </head>
<body> <body>
@ -84,23 +84,23 @@
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade"> <div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <form class="modal-content" action="javascript:" onsubmit="addPlayer()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">add new player</h4> <h4 class="modal-title">add new player</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div> </div>
<form class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="nick">player name</label> <label class="col-sm-4 col-form-label" for="nick">player name</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="nick" name="nick" class="form-control" placeholder="nick" onkeyup="disableAddPlayerForm()"> <input id="nick" name="nick" class="form-control" placeholder="nick" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="job">player job</label> <label class="col-sm-4 col-form-label" for="job">player job</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="job" name="job" class="form-control" title="job"></select> <select id="job" name="job" class="form-control" title="job" required></select>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
@ -115,13 +115,13 @@
<input id="priority" name="priority" type="number" class="form-control" value="0"> <input id="priority" name="priority" type="number" class="form-control" value="0">
</div> </div>
</div> </div>
</form> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-player-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addPlayer()" disabled>add</button> <button type="submit" class="btn btn-primary">add</button>
</div>
</div> </div>
</form>
</div> </div>
</div> </div>
@ -129,6 +129,7 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"> <ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li> <li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul> </ul>
<ul class="nav"> <ul class="nav">
@ -162,7 +163,6 @@
const removeButton = $("#remove-btn"); const removeButton = $("#remove-btn");
const addPlayerDialog = $("#add-player-dialog"); const addPlayerDialog = $("#add-player-dialog");
const submitPlayerButton = $("#submit-player-btn");
const jobInput = $("#job"); const jobInput = $("#job");
const linkInput = $("#link"); const linkInput = $("#link");
@ -187,6 +187,8 @@
success: function (_) { reload(); }, success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
addPlayerDialog.modal("hide");
return true; // action expects boolean result
} }
function bisLinkFormatter(link, row) { function bisLinkFormatter(link, row) {
@ -197,10 +199,6 @@
} }
} }
function disableAddPlayerForm() {
submitPlayerButton.attr("disabled", !nickInput.val());
}
function hideControls() { function hideControls() {
addButton.attr("hidden", isReadOnly); addButton.attr("hidden", isReadOnly);
removeButton.attr("hidden", isReadOnly); removeButton.attr("hidden", isReadOnly);

View File

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FFXIV loot helper API</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet" type="text/css">
<link href="/static/favicon.ico" rel="shortcut icon">
</head>
<body>
<redoc spec-url="/api-docs/swagger.json"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
</body>
</html>

View File

@ -6,17 +6,17 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/favicon.ico" rel="shortcut icon"> <link rel="shortcut icon" href="/static/favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" type="text/css">
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" type="text/css">
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" type="text/css">
<link href="/static/styles.css" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="/static/styles.css" type="text/css">
</head> </head>
<body> <body>
@ -84,38 +84,38 @@
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade"> <div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <form class="modal-content" action="javascript:" onsubmit="addUser()">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">add new user</h4> <h4 class="modal-title">add new user</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div> </div>
<form class="modal-body"> <div class="modal-body">
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="username">login</label> <label class="col-sm-4 col-form-label" for="username">login</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="username" name="username" class="form-control" placeholder="username" onkeyup="disableAddUserForm()"> <input id="username" name="username" class="form-control" placeholder="username" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="password">password</label> <label class="col-sm-4 col-form-label" for="password">password</label>
<div class="col-sm-8"> <div class="col-sm-8">
<input id="password" name="password" type="password" class="form-control" placeholder="password" onkeyup="disableAddUserForm()"> <input id="password" name="password" type="password" class="form-control" placeholder="password" required>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-sm-4 col-form-label" for="permission">permission</label> <label class="col-sm-4 col-form-label" for="permission">permission</label>
<div class="col-sm-8"> <div class="col-sm-8">
<select id="permission" name="permission" class="form-control" title="permission"></select> <select id="permission" name="permission" class="form-control" title="permission" required></select>
</div>
</div> </div>
</div> </div>
</form>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button> <button type="button" class="btn btn-danger" data-bs-dismiss="modal">close</button>
<button id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addUser()" disabled>add</button> <button type="submit" class="btn btn-primary">add</button>
</div>
</div> </div>
</form>
</div> </div>
</div> </div>
@ -123,6 +123,7 @@
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top"> <footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
<ul class="nav"> <ul class="nav">
<li><a class="nav-link" href="/" title="home">home</a></li> <li><a class="nav-link" href="/" title="home">home</a></li>
<li><a class="nav-link" href="/api-docs" title="api">api</a></li>
</ul> </ul>
<ul class="nav"> <ul class="nav">
@ -156,7 +157,6 @@
const removeButton = $("#remove-btn"); const removeButton = $("#remove-btn");
const addUserDialog = $("#add-user-dialog"); const addUserDialog = $("#add-user-dialog");
const submitUserButton = $("#submit-btn");
const usernameInput = $("#username"); const usernameInput = $("#username");
const passwordInput = $("#password"); const passwordInput = $("#password");
@ -176,10 +176,8 @@
success: function (_) { reload(); }, success: function (_) { reload(); },
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); }, error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
}); });
} addUserDialog.modal("hide");
return true; // action expects boolean result
function disableAddUserForm() {
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
} }
function hideControls() { function hideControls() {

View File

@ -50,6 +50,7 @@ me.arcanis.ffxivbis {
#hostname = "127.0.0.1:8000" #hostname = "127.0.0.1:8000"
# enable head requests for GET requests # enable head requests for GET requests
enable-head-requests = yes enable-head-requests = yes
schemes = ["http"]
authorization-cache { authorization-cache {
# maximum amount of cached logins # maximum amount of cached logins

View File

@ -37,7 +37,7 @@ function loadTypes(url, selector) {
} }
function setupFormClear(dialog, reset) { function setupFormClear(dialog, reset) {
dialog.on("shown.bs.modal", function () { dialog.on("hide.bs.modal", function () {
$(this).find("form").trigger("reset"); $(this).find("form").trigger("reset");
$(this).find("table").bootstrapTable("removeAll"); $(this).find("table").bootstrapTable("removeAll");
if (reset) { if (reset) {

View File

@ -50,7 +50,7 @@ class Application(context: ActorContext[Nothing]) extends AbstractBehavior[Nothi
val party = context.spawn(PartyService(storage), "party") val party = context.spawn(PartyService(storage), "party")
val http = new RootEndpoint(context.system, party, bisProvider) val http = new RootEndpoint(context.system, party, bisProvider)
val flow = Route.toFlow(http.route)(context.system) val flow = Route.toFlow(http.routes)(context.system)
Http(context.system).newServerAt(host, port).bindFlow(flow) Http(context.system).newServerAt(host, port).bindFlow(flow)
case Success(result) => case Success(result) =>

View File

@ -12,6 +12,7 @@ import akka.actor.typed.{ActorRef, ActorSystem, Scheduler}
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import akka.util.Timeout import akka.util.Timeout
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.cors
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint import me.arcanis.ffxivbis.http.api.v1.RootApiV1Endpoint
import me.arcanis.ffxivbis.http.view.RootView import me.arcanis.ffxivbis.http.view.RootView
@ -33,28 +34,30 @@ class RootEndpoint(system: ActorSystem[Nothing], storage: ActorRef[Message], pro
private val rootView = new RootView(auth) private val rootView = new RootView(auth)
private val swagger = new Swagger(config) private val swagger = new Swagger(config)
def route: Route = def routes: Route =
withHttpLog { withHttpLog {
ignoreTrailingSlash { ignoreTrailingSlash {
apiRoute ~ htmlRoute ~ swagger.routes ~ swaggerUIRoute cors() {
apiRoutes ~ htmlRoutes ~ swagger.routes ~ swaggerUIRoutes
}
} }
} }
private def apiRoute: Route = private def apiRoutes: Route =
pathPrefix("api") { pathPrefix("api") {
pathPrefix(Segment) { pathPrefix(Segment) {
case "v1" => rootApiV1Endpoint.route case "v1" => rootApiV1Endpoint.routes
case _ => reject case _ => reject
} }
} }
private def htmlRoute: Route = private def htmlRoutes: Route =
pathPrefix("static") { pathPrefix("static") {
getFromResourceDirectory("static") getFromResourceDirectory("static")
} ~ rootView.route } ~ rootView.routes
private def swaggerUIRoute: Route = private def swaggerUIRoutes: Route =
path("api-docs") { path("api-docs") {
getFromResource("html/redoc.html") getFromResource("html/api.html")
} }
} }

View File

@ -14,6 +14,7 @@ import com.typesafe.config.Config
import io.swagger.v3.oas.models.security.SecurityScheme import io.swagger.v3.oas.models.security.SecurityScheme
import scala.io.Source import scala.io.Source
import scala.jdk.CollectionConverters._
class Swagger(config: Config) extends SwaggerHttpService { class Swagger(config: Config) extends SwaggerHttpService {
@ -38,13 +39,15 @@ class Swagger(config: Config) extends SwaggerHttpService {
if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname") if (config.hasPath("me.arcanis.ffxivbis.web.hostname")) config.getString("me.arcanis.ffxivbis.web.hostname")
else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getInt("me.arcanis.ffxivbis.web.port")}" else s"${config.getString("me.arcanis.ffxivbis.web.host")}:${config.getInt("me.arcanis.ffxivbis.web.port")}"
override val schemes: List[String] = config.getStringList("me.arcanis.ffxivbis.web.schemes").asScala.toList
private val basicAuth = new SecurityScheme() private val basicAuth = new SecurityScheme()
.description("basic http auth") .description("basic http auth")
.`type`(SecurityScheme.Type.HTTP) .`type`(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.scheme("bearer") .scheme("basic")
override val securitySchemes: Map[String, SecurityScheme] = Map("basic auth" -> basicAuth) override val securitySchemes: Map[String, SecurityScheme] = Map("basic" -> basicAuth)
override val unwantedDefinitions: Seq[String] = override val unwantedDefinitions: Seq[String] =
Seq("Function1", "Function1RequestContextFutureRouteResult") Seq("Function1", "Function1RequestContextFutureRouteResult", "SeqLootModel", "SeqPieceModel")
} }

View File

@ -40,7 +40,7 @@ class BiSEndpoint(
with Authorization with Authorization
with JsonSupport { with JsonSupport {
def route: Route = createBiS ~ getBiS ~ modifyBiS def routes: Route = createBiS ~ getBiS ~ modifyBiS
@PUT @PUT
@Path("party/{partyId}/bis") @Path("party/{partyId}/bis")
@ -79,7 +79,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
tags = Array("best in slot"), tags = Array("best in slot"),
) )
def createBiS: Route = def createBiS: Route =
@ -140,7 +140,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("best in slot"), tags = Array("best in slot"),
) )
def getBiS: Route = def getBiS: Route =
@ -197,7 +197,7 @@ class BiSEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
tags = Array("best in slot"), tags = Array("best in slot"),
) )
def modifyBiS: Route = def modifyBiS: Route =

View File

@ -11,6 +11,7 @@ package me.arcanis.ffxivbis.http.api.v1
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._ import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._ import akka.http.scaladsl.server._
import ch.megard.akka.http.cors.scaladsl.CorsDirectives.corsRejectionHandler
import com.typesafe.scalalogging.StrictLogging import com.typesafe.scalalogging.StrictLogging
import me.arcanis.ffxivbis.http.api.v1.json._ import me.arcanis.ffxivbis.http.api.v1.json._
import spray.json._ import spray.json._
@ -27,9 +28,26 @@ trait HttpHandler extends StrictLogging { this: JsonSupport =>
} }
def rejectionHandler: RejectionHandler = def rejectionHandler: RejectionHandler =
RejectionHandler.default RejectionHandler
.newBuilder()
.handleAll[MethodRejection] { rejections =>
val (methods, names) = rejections.map(r => r.supported -> r.supported.name).unzip
respondWithHeader(headers.Allow(methods)) {
options {
complete(StatusCodes.OK, HttpEntity.Empty)
} ~
complete(
StatusCodes.MethodNotAllowed,
s"HTTP method not allowed, supported methods: ${names.mkString(", ")}"
)
}
}
.result()
.withFallback(corsRejectionHandler)
.seal
.mapRejectionResponse { .mapRejectionResponse {
case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) => case response @ HttpResponse(_, _, entity: HttpEntity.Strict, _) if entity.data.nonEmpty =>
val message = ErrorModel(entity.data.utf8String).toJson val message = ErrorModel(entity.data.utf8String).toJson
response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint)) response.withEntity(HttpEntity(ContentTypes.`application/json`, message.compactPrint))
case other => other case other => other

View File

@ -37,7 +37,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
with JsonSupport with JsonSupport
with HttpHandler { with HttpHandler {
def route: Route = getLoot ~ modifyLoot ~ suggestLoot def routes: Route = getLoot ~ modifyLoot ~ suggestLoot
@GET @GET
@Path("party/{partyId}/loot") @Path("party/{partyId}/loot")
@ -81,7 +81,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("loot"), tags = Array("loot"),
) )
def getLoot: Route = def getLoot: Route =
@ -137,7 +137,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
tags = Array("loot"), tags = Array("loot"),
) )
def modifyLoot: Route = def modifyLoot: Route =
@ -202,7 +202,7 @@ class LootEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("loot"), tags = Array("loot"),
) )
def suggestLoot: Route = def suggestLoot: Route =

View File

@ -40,7 +40,7 @@ class PartyEndpoint(
with JsonSupport with JsonSupport
with HttpHandler { with HttpHandler {
def route: Route = getPartyDescription ~ modifyPartyDescription def routes: Route = getPartyDescription ~ modifyPartyDescription
@GET @GET
@Path("party/{partyId}/description") @Path("party/{partyId}/description")
@ -73,7 +73,7 @@ class PartyEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("party"), tags = Array("party"),
) )
def getPartyDescription: Route = def getPartyDescription: Route =
@ -126,7 +126,7 @@ class PartyEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
tags = Array("party"), tags = Array("party"),
) )
def modifyPartyDescription: Route = def modifyPartyDescription: Route =

View File

@ -41,7 +41,7 @@ class PlayerEndpoint(
with JsonSupport with JsonSupport
with HttpHandler { with HttpHandler {
def route: Route = getParty ~ getPartyStats ~ modifyParty def routes: Route = getParty ~ getPartyStats ~ modifyParty
@GET @GET
@Path("party/{partyId}") @Path("party/{partyId}")
@ -85,7 +85,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("party"), tags = Array("party"),
) )
def getParty: Route = def getParty: Route =
@ -146,7 +146,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("party"), tags = Array("party"),
) )
def getPartyStats: Route = def getPartyStats: Route =
@ -202,7 +202,7 @@ class PlayerEndpoint(
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("post"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("post"))),
tags = Array("party"), tags = Array("party"),
) )
def modifyParty: Route = def modifyParty: Route =

View File

@ -36,11 +36,11 @@ class RootApiV1Endpoint(
private val typesEndpoint = new TypesEndpoint(config) private val typesEndpoint = new TypesEndpoint(config)
private val userEndpoint = new UserEndpoint(storage, auth) private val userEndpoint = new UserEndpoint(storage, auth)
def route: Route = def routes: Route =
handleExceptions(exceptionHandler) { handleExceptions(exceptionHandler) {
handleRejections(rejectionHandler) { handleRejections(rejectionHandler) {
biSEndpoint.route ~ lootEndpoint.route ~ partyEndpoint.route ~ playerEndpoint.route ~ biSEndpoint.routes ~ lootEndpoint.routes ~ partyEndpoint.routes ~ playerEndpoint.routes ~
statusEndpoint.route ~ typesEndpoint.route ~ userEndpoint.route statusEndpoint.routes ~ typesEndpoint.routes ~ userEndpoint.routes
} }
} }
} }

View File

@ -19,7 +19,7 @@ import me.arcanis.ffxivbis.http.api.v1.json._
@Path("/api/v1") @Path("/api/v1")
class StatusEndpoint extends JsonSupport { class StatusEndpoint extends JsonSupport {
def route: Route = getServerStatus def routes: Route = getServerStatus
@GET @GET
@Path("status") @Path("status")

View File

@ -21,7 +21,7 @@ import me.arcanis.ffxivbis.models._
@Path("/api/v1") @Path("/api/v1")
class TypesEndpoint(config: Config) extends JsonSupport { class TypesEndpoint(config: Config) extends JsonSupport {
def route: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority def routes: Route = getAllJobs ~ getJobs ~ getPermissions ~ getPieces ~ getPieceTypes ~ getPriority
@GET @GET
@Path("types/jobs/all") @Path("types/jobs/all")

View File

@ -36,9 +36,9 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
with Authorization with Authorization
with JsonSupport { with JsonSupport {
def route: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent def routes: Route = createParty ~ createUser ~ deleteUser ~ getUsers ~ getUsersCurrent
@PUT @POST
@Path("party") @Path("party")
@Consumes(value = Array("application/json")) @Consumes(value = Array("application/json"))
@Operation( @Operation(
@ -76,7 +76,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
def createParty: Route = def createParty: Route =
path("party") { path("party") {
extractExecutionContext { implicit executionContext => extractExecutionContext { implicit executionContext =>
put { post {
entity(as[UserModel]) { user => entity(as[UserModel]) { user =>
onSuccess(newPartyId) { partyId => onSuccess(newPartyId) { partyId =>
val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin) val admin = user.toUser.copy(partyId = partyId, permission = Permission.admin)
@ -126,7 +126,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) 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", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),
) )
def createUser: Route = def createUser: Route =
@ -172,7 +172,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) 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", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),
) )
def deleteUser: Route = def deleteUser: Route =
@ -223,7 +223,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel])))
), ),
), ),
security = Array(new SecurityRequirement(name = "basic auth", scopes = Array("get"))), security = Array(new SecurityRequirement(name = "basic", scopes = Array("get"))),
tags = Array("users"), tags = Array("users"),
) )
def getUsers: Route = def getUsers: Route =
@ -270,7 +270,7 @@ class UserEndpoint(override val storage: ActorRef[Message], override val auth: A
content = Array(new Content(schema = new Schema(implementation = classOf[ErrorModel]))) 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", scopes = Array("admin"))),
tags = Array("users"), tags = Array("users"),
) )
def getUsersCurrent: Route = def getUsersCurrent: Route =

View File

@ -19,7 +19,10 @@ case class PlayerModel(
@Schema(description = "looted pieces") loot: Option[Seq[LootModel]], @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 = "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 = "count of looted pieces which are parts of best in slot",
`type` = "number"
) lootCountBiS: Option[Int],
@Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int], @Schema(description = "total count of looted pieces", `type` = "number") lootCountTotal: Option[Int],
) { ) {

View File

@ -15,7 +15,7 @@ import me.arcanis.ffxivbis.http.{Authorization, AuthorizationProvider}
class RootView(override val auth: AuthorizationProvider) extends Authorization { class RootView(override val auth: AuthorizationProvider) extends Authorization {
def route: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers def routes: Route = getBiS ~ getIndex ~ getLoot ~ getParty ~ getUsers
def getBiS: Route = def getBiS: Route =
path("party" / Segment / "bis") { partyId: String => path("party" / Segment / "bis") { partyId: String =>

View File

@ -8,13 +8,15 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseBiSHandler { this: Database => trait DatabaseBiSHandler { this: Database =>
def bisHandler: DatabaseMessage.Handler = { def bisHandler(msg: BisDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceToBis(playerId, piece, client) => case AddPieceToBis(playerId, piece, client) =>
run(profile.insertPieceBiS(playerId, piece))(_ => client ! ()) run(profile.insertPieceBiS(playerId, piece))(_ => client ! ())
Behaviors.same Behaviors.same

View File

@ -11,7 +11,7 @@ package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext} import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext}
import akka.actor.typed.{Behavior, DispatcherSelector} import akka.actor.typed.{Behavior, DispatcherSelector}
import com.typesafe.config.Config import com.typesafe.config.Config
import me.arcanis.ffxivbis.messages.DatabaseMessage import me.arcanis.ffxivbis.messages.{BisDatabaseMessage, DatabaseMessage, LootDatabaseMessage, PartyDatabaseMessage, UserDatabaseMessage}
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
import me.arcanis.ffxivbis.storage.DatabaseProfile import me.arcanis.ffxivbis.storage.DatabaseProfile
@ -32,8 +32,12 @@ class DatabaseImpl(context: ActorContext[DatabaseMessage])
override val config: Config = context.system.settings.config override val config: Config = context.system.settings.config
override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config) override val profile: DatabaseProfile = new DatabaseProfile(executionContext, config)
override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] = handle(msg) override def onMessage(msg: DatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case msg: BisDatabaseMessage => bisHandler(msg)
case msg: LootDatabaseMessage => lootHandler(msg)
case msg: PartyDatabaseMessage => partyHandler(msg)
case msg: UserDatabaseMessage => userHandler(msg)
}
private def handle: DatabaseMessage.Handler =
bisHandler.orElse(lootHandler).orElse(partyHandler).orElse(userHandler)
} }

View File

@ -8,6 +8,7 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.Loot import me.arcanis.ffxivbis.models.Loot
@ -17,7 +18,8 @@ import java.time.Instant
trait DatabaseLootHandler { this: Database => trait DatabaseLootHandler { this: Database =>
def lootHandler: DatabaseMessage.Handler = { def lootHandler(msg: LootDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPieceTo(playerId, piece, isFreeLoot, client) => case AddPieceTo(playerId, piece, isFreeLoot, client) =>
val loot = Loot(-1, piece, Instant.now, isFreeLoot) val loot = Loot(-1, piece, Instant.now, isFreeLoot)
run(profile.insertPiece(playerId, loot))(_ => client ! ()) run(profile.insertPiece(playerId, loot))(_ => client ! ())

View File

@ -8,6 +8,7 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.models.{BiS, Player} import me.arcanis.ffxivbis.models.{BiS, Player}
@ -17,7 +18,8 @@ import scala.concurrent.Future
trait DatabasePartyHandler { this: Database => trait DatabasePartyHandler { this: Database =>
def partyHandler: DatabaseMessage.Handler = { def partyHandler(msg: PartyDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddPlayer(player, client) => case AddPlayer(player, client) =>
run(profile.insertPlayer(player))(_ => client ! ()) run(profile.insertPlayer(player))(_ => client ! ())
Behaviors.same Behaviors.same

View File

@ -8,13 +8,15 @@
*/ */
package me.arcanis.ffxivbis.service.database.impl package me.arcanis.ffxivbis.service.database.impl
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors import akka.actor.typed.scaladsl.Behaviors
import me.arcanis.ffxivbis.messages._ import me.arcanis.ffxivbis.messages._
import me.arcanis.ffxivbis.service.database.Database import me.arcanis.ffxivbis.service.database.Database
trait DatabaseUserHandler { this: Database => trait DatabaseUserHandler { this: Database =>
def userHandler: DatabaseMessage.Handler = { def userHandler(msg: UserDatabaseMessage): Behavior[DatabaseMessage] =
msg match {
case AddUser(user, isHashedPassword, client) => case AddUser(user, isHashedPassword, client) =>
val toInsert = if (isHashedPassword) user else user.withHashedPassword val toInsert = if (isHashedPassword) user else user.withHashedPassword
run(profile.insertUser(toInsert))(_ => client ! ()) run(profile.insertUser(toInsert))(_ => client ! ())

View File

@ -37,7 +37,7 @@ class BiSEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRouteT
private val storage = testKit.spawn(Database()) private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider()) private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route private val route = new BiSEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()

View File

@ -34,7 +34,7 @@ class LootEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private val storage = testKit.spawn(Database()) private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route private val route = new LootEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()

View File

@ -35,7 +35,7 @@ class PartyEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRout
private val storage = testKit.spawn(Database()) private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider()) private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route private val route = new PartyEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()

View File

@ -34,7 +34,7 @@ class PlayerEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRou
private val storage = testKit.spawn(Database()) private val storage = testKit.spawn(Database())
private val provider = testKit.spawn(BisProvider()) private val provider = testKit.spawn(BisProvider())
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).route private val route = new PlayerEndpoint(party, provider, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()

View File

@ -15,7 +15,7 @@ class StatusEndpointTest extends AnyWordSpecLike
override val testConfig: Config = Settings.withRandomDatabase override val testConfig: Config = Settings.withRandomDatabase
private val route = new StatusEndpoint().route private val route = new StatusEndpoint().routes
"api v1 status endpoint" must { "api v1 status endpoint" must {

View File

@ -16,7 +16,7 @@ class TypesEndpointTest extends AnyWordSpecLike
override val testConfig: Config = Settings.withRandomDatabase override val testConfig: Config = Settings.withRandomDatabase
private val route = new TypesEndpoint(testConfig).route private val route = new TypesEndpoint(testConfig).routes
"api v1 types endpoint" must { "api v1 types endpoint" must {

View File

@ -30,7 +30,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
private var partyId = Fixtures.partyId private var partyId = Fixtures.partyId
private val storage = testKit.spawn(Database()) private val storage = testKit.spawn(Database())
private val party = testKit.spawn(PartyService(storage)) private val party = testKit.spawn(PartyService(storage))
private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).route private val route = new UserEndpoint(party, Fixtures.authProvider)(askTimeout, testKit.scheduler).routes
override def beforeAll(): Unit = { override def beforeAll(): Unit = {
super.beforeAll() super.beforeAll()
@ -50,7 +50,7 @@ class UserEndpointTest extends AnyWordSpecLike with Matchers with ScalatestRoute
val uri = Uri(s"/party") val uri = Uri(s"/party")
val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword) val entity = UserModel.fromUser(Fixtures.userAdmin).copy(password = Fixtures.userPassword)
Put(uri, entity) ~> route ~> check { Post(uri, entity) ~> route ~> check {
status shouldEqual StatusCodes.OK status shouldEqual StatusCodes.OK
partyId = responseAs[PartyIdModel].partyId partyId = responseAs[PartyIdModel].partyId
} }

View File

@ -1 +1 @@
version := "0.12.0" version := "0.13.2"