mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-24 17:27:17 +00:00
migrate to bootstrap
This commit is contained in:
parent
a6991a0a91
commit
9ef7c3ef34
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@ -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:
|
||||
|
4
.github/workflows/run-tests.yml
vendored
4
.github/workflows/run-tests.yml
vendored
@ -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
|
||||
run: make tests
|
35
Makefile
Normal file
35
Makefile
Normal file
@ -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
|
@ -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"
|
||||
|
349
src/main/resources/html/bis.html
Normal file
349
src/main/resources/html/bis.html
Normal file
@ -0,0 +1,349 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Best in slot</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Best in slot</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="update-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#update-bis-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> update
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePiece()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="bis" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "bis"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="piece">piece</th>
|
||||
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="update-bis-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="player" name="player" class="form-control" title="player"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="piece-row" class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece" name="piece" class="form-control" title="piece"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="piece-type-row" class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece-type" name="pieceType" class="form-control" title="piece-type"></select>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div class="col-sm-8">
|
||||
<input id="bis-link" name="link" class="form-control" placeholder="link to bis" onkeyup="disableSubmitBisButton()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<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-update-bis-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="setBis()" style="display: none" disabled>set</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
</ul>
|
||||
|
||||
<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/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#bis");
|
||||
const removeButton = $("#remove-btn");
|
||||
const updateButton = $("#update-btn");
|
||||
|
||||
const submitAddBisButton = $("#submit-add-bis-btn");
|
||||
const submitUpdateBisButton = $("#submit-update-bis-btn");
|
||||
const updateBisDialog = $("#update-bis-dialog");
|
||||
|
||||
const addPieceButton = $("#add-piece-btn");
|
||||
const updateBisButton = $("#update-bis-btn");
|
||||
|
||||
const bisLinkRow = $("#bis-link-row");
|
||||
const pieceRow = $("#piece-row");
|
||||
const pieceTypeRow = $("#piece-type-row");
|
||||
|
||||
const linkInput = $("#bis-link");
|
||||
const pieceInput = $("#piece");
|
||||
const pieceTypeInput = $("#piece-type");
|
||||
const playerInput = $("#player");
|
||||
|
||||
function addPiece() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
piece: {
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: player.dataset.job,
|
||||
piece: pieceInput.val(),
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableSubmitBisButton() {
|
||||
const nonEmpty = (playerInput.val() !== null); // well lol
|
||||
submitUpdateBisButton.attr("disabled", !(nonEmpty && linkInput.val()));
|
||||
submitAddBisButton.attr("disabled", !(nonEmpty));
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
updateButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function hideLinkPart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.hide();
|
||||
submitUpdateBisButton.hide();
|
||||
pieceRow.show();
|
||||
pieceTypeRow.show();
|
||||
submitAddBisButton.show();
|
||||
}
|
||||
|
||||
function hidePiecePart() {
|
||||
disableSubmitBisButton();
|
||||
bisLinkRow.show();
|
||||
submitUpdateBisButton.show();
|
||||
pieceRow.hide();
|
||||
pieceTypeRow.hide();
|
||||
submitAddBisButton.hide();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.bis.map(function (loot) {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
piece: loot.piece,
|
||||
pieceType: loot.pieceType,
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (player) {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
option.dataset.job = player.job;
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
disableSubmitBisButton();
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePiece() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (loot) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
piece: {
|
||||
pieceType: loot.pieceType,
|
||||
job: loot.job,
|
||||
piece: loot.piece,
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: loot.job,
|
||||
nick: loot.nick,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if (updateBisButton.is(":checked")) {
|
||||
hidePiecePart();
|
||||
}
|
||||
if (addPieceButton.is(":checked")) {
|
||||
hideLinkPart();
|
||||
}
|
||||
}
|
||||
|
||||
function setBis() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/bis`,
|
||||
data: JSON.stringify({
|
||||
link: linkInput.val(),
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
},
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(updateBisDialog, reset);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
updateBisButton.click(function () { reset(); });
|
||||
addPieceButton.click(function () { reset(); });
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
183
src/main/resources/html/index.html
Normal file
183
src/main/resources/html/index.html
Normal file
@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FFXIV loot helper</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<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 href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container mb-5">
|
||||
<div class="form-group row">
|
||||
<div class="btn-group" role="group" aria-label="Sign in">
|
||||
<input id="signin-btn" name="signin" type="radio" class="btn-check" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-primary" for="signin-btn">login to existing party</label>
|
||||
|
||||
<input id="signup-btn" name="signin" type="radio" class="btn-check" autocomplete="off">
|
||||
<label class="btn btn-outline-primary" for="signup-btn">create a new party</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="signup-form" class="container mb-5" style="display: none">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="alias">party alias</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="alias" name="alias" class="form-control" placeholder="alias">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="username">username</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="username" name="username" class="form-control" placeholder="admin user name" onkeyup="disableAddButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="password" class="col-sm-2 col-form-label">password</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="admin password" onkeyup="disableAddButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button id="add-btn" type="button" class="btn btn-primary" onclick="createParty()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="signin-form" class="container mb-5">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="party-id">party id</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="party-id" name="partyId" class="form-control" placeholder="id" onkeyup="disableRedirectButton()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button id="redirect-btn" type="button" class="btn btn-primary" onclick="redirectToParty()" disabled>go</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="container">
|
||||
<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="https://github.com/arcan1s/ffxivbis" title="sources">ffxivbis</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
|
||||
<script>
|
||||
const signinButton = $("#signin-btn");
|
||||
const signupButton = $("#signup-btn");
|
||||
|
||||
const addButton = $("#add-btn");
|
||||
const redirectButton = $("#redirect-btn");
|
||||
const signinForm = $("#signin-form");
|
||||
const signupForm = $("#signup-form");
|
||||
|
||||
const aliasInput = $("#alias");
|
||||
const partyIdInput = $("#party-id");
|
||||
const passwordInput = $("#password");
|
||||
const usernameInput = $("#username");
|
||||
|
||||
function createDescription(partyId) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/description`,
|
||||
data: JSON.stringify({
|
||||
partyId: partyId,
|
||||
partyAlias: aliasInput.val(),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { doRedirect(partyId); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function createParty() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party`,
|
||||
data: JSON.stringify({
|
||||
partyId: "",
|
||||
username: usernameInput.val(),
|
||||
password: passwordInput.val(),
|
||||
permission: "admin",
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
if (aliasInput.val()) {
|
||||
createDescription(data.partyId);
|
||||
} else {
|
||||
doRedirect(data.partyId);
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableAddButton() {
|
||||
addButton.attr("disabled", !(passwordInput.val() && usernameInput.val()));
|
||||
}
|
||||
|
||||
function disableRedirectButton() {
|
||||
redirectButton.attr("disabled", !partyIdInput.val());
|
||||
}
|
||||
|
||||
function doRedirect(partyId) {
|
||||
location.href = `/party/${partyId}`;
|
||||
}
|
||||
|
||||
function hideSigninPart() {
|
||||
signinForm.hide();
|
||||
signupForm.show();
|
||||
}
|
||||
|
||||
function hideSignupPart() {
|
||||
signinForm.show();
|
||||
signupForm.hide();
|
||||
}
|
||||
|
||||
function redirectToParty() {
|
||||
return doRedirect(partyIdInput.val());
|
||||
}
|
||||
|
||||
function reset() {
|
||||
signinForm.trigger("reset");
|
||||
signupForm.trigger("reset");
|
||||
if (signinButton.is(":checked")) {
|
||||
hideSignupPart();
|
||||
}
|
||||
if (signupButton.is(":checked")) {
|
||||
hideSigninPart();
|
||||
}
|
||||
}
|
||||
|
||||
$(function () {
|
||||
signinButton.click(function () { reset(); });
|
||||
signupButton.click(function () { reset(); });
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
338
src/main/resources/html/loot.html
Normal file
338
src/main/resources/html/loot.html
Normal file
@ -0,0 +1,338 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Loot table</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Looted items</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-loot-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removeLoot()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="loot" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "loot"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="piece">piece</th>
|
||||
<th data-sortable="true" data-field="pieceType">piece type</th>
|
||||
<th data-sortable="true" data-field="isFreeLoot">is free loot</th>
|
||||
<th data-sortable="true" data-field="timestamp">date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-loot-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add looted piece</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<form class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="player">player</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="player" name="player" class="form-control" title="player"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece">piece</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece" name="piece" class="form-control" title="piece"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="piece-type">piece type</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="piece-type" name="pieceType" class="form-control" title="pieceType"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="job">job</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="job" name="job" class="form-control" title="job"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table id="stats" class="table table-striped table-hover">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-field="nick">nick</th>
|
||||
<th data-field="job">job</th>
|
||||
<th data-field="isRequired">required</th>
|
||||
<th data-field="lootCount">these pieces looted</th>
|
||||
<th data-field="lootCountBiS">total bis pieces looted</th>
|
||||
<th data-field="lootCountTotal">total pieces looted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<div class="form-check form-switch">
|
||||
<input id="free-loot" name="freeLoot" type="checkbox" class="form-check-input">
|
||||
<label class="form-check-label" for="free-loot">as free loot</label>
|
||||
</div>
|
||||
<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 id="submit-btn" type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="addLoot()" disabled>add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
</ul>
|
||||
|
||||
<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/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#loot");
|
||||
const stats = $("#stats");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const submitLootButton = $("#submit-btn");
|
||||
const addLootDialog = $("#add-loot-dialog");
|
||||
|
||||
const freeLootInput = $("#free-loot");
|
||||
const jobInput = $("#job");
|
||||
const pieceInput = $("#piece");
|
||||
const pieceTypeInput = $("#piece-type");
|
||||
const playerInput = $("#player");
|
||||
|
||||
function addLoot() {
|
||||
const player = getCurrentOption(playerInput);
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
piece: {
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: player.dataset.job,
|
||||
piece: pieceInput.val(),
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: player.dataset.nick,
|
||||
job: player.dataset.job,
|
||||
},
|
||||
isFreeLoot: freeLootInput.is(":checked"),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const items = data.map(function (player) {
|
||||
return player.loot.map(function (loot) {
|
||||
return {
|
||||
nick: player.nick,
|
||||
job: player.job,
|
||||
piece: loot.piece.piece,
|
||||
pieceType: loot.piece.pieceType,
|
||||
isFreeLoot: loot.isFreeLoot ? "yes" : "no",
|
||||
timestamp: loot.timestamp,
|
||||
};
|
||||
});
|
||||
});
|
||||
const payload = items.reduce(function (left, right) { return left.concat(right); }, []);
|
||||
table.bootstrapTable("load", payload);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
|
||||
const options = data.map(function (player) {
|
||||
const option = document.createElement("option");
|
||||
option.innerText = formatPlayerId(player);
|
||||
option.dataset.nick = player.nick;
|
||||
option.dataset.job = player.job;
|
||||
return option;
|
||||
});
|
||||
playerInput.empty().append(options);
|
||||
submitLootButton.attr("disabled", options.length === 0);
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeLoot() {
|
||||
const pieces = table.bootstrapTable("getSelections");
|
||||
pieces.map(function (loot) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
piece: {
|
||||
pieceType: loot.pieceType,
|
||||
job: loot.job,
|
||||
piece: loot.piece,
|
||||
},
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
nick: loot.nick,
|
||||
job: loot.job,
|
||||
},
|
||||
isFreeLoot: loot.isFreeLoot === "yes",
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function suggestLoot() {
|
||||
stats.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/loot`,
|
||||
data: JSON.stringify({
|
||||
pieceType: pieceTypeInput.val(),
|
||||
job: jobInput.val(),
|
||||
piece: pieceInput.val(),
|
||||
}),
|
||||
type: "PUT",
|
||||
contentType: "application/json",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
const payload = data.map(function (stat) {
|
||||
return {
|
||||
nick: stat.nick,
|
||||
job: stat.job,
|
||||
isRequired: stat.isRequired,
|
||||
lootCount: stat.lootCount,
|
||||
lootCountBiS: stat.lootCountBiS,
|
||||
lootCountTotal: stat.lootCountTotal,
|
||||
};
|
||||
});
|
||||
stats.bootstrapTable("load", payload);
|
||||
stats.bootstrapTable("uncheckAll");
|
||||
stats.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addLootDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs/all", jobInput);
|
||||
loadTypes("/api/v1/types/pieces", pieceInput);
|
||||
loadTypes("/api/v1/types/pieces/types", pieceTypeInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
stats.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
258
src/main/resources/html/party.html
Normal file
258
src/main/resources/html/party.html
Normal file
@ -0,0 +1,258 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FFXIV loot helper</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-player-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removePlayers()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="players" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "players"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="nick">nick</th>
|
||||
<th data-sortable="true" data-field="job">job</th>
|
||||
<th data-sortable="true" data-field="link" data-formatter="bisLinkFormatter">best in slot link</th>
|
||||
<th data-sortable="true" data-field="lootCountBiS">total bis pieces looted</th>
|
||||
<th data-sortable="true" data-field="lootCountTotal">total pieces looted</th>
|
||||
<th data-sortable="true" data-field="priority">priority</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-player-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add new player</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<form class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="nick">player name</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="nick" name="nick" class="form-control" placeholder="nick" onkeyup="disableAddPlayerForm()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="job">player job</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="job" name="job" class="form-control" title="job"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="link">link to best in slot</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="link" name="link" class="form-control" placeholder="link to bis">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="priority">priority</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="priority" name="priority" type="number" class="form-control" value="0">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
</ul>
|
||||
|
||||
<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/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#players");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addPlayerDialog = $("#add-player-dialog");
|
||||
const submitPlayerButton = $("#submit-player-btn");
|
||||
|
||||
const jobInput = $("#job");
|
||||
const linkInput = $("#link");
|
||||
const nickInput = $("#nick");
|
||||
const priorityInput = $("#priority");
|
||||
|
||||
function addPlayer() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
data: JSON.stringify({
|
||||
action: "add",
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: jobInput.val(),
|
||||
nick: nickInput.val(),
|
||||
link: linkInput.val() || null,
|
||||
priority: parseInt(priorityInput.val(), 10),
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function bisLinkFormatter(link, row) {
|
||||
if (link) {
|
||||
return `<a href="${safe(link)}" title="${safe(row.nick)} best in slot for ${safe(row.job)}">${safe(link)}</a>`;
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
function disableAddPlayerForm() {
|
||||
submitPlayerButton.attr("disabled", !nickInput.val());
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removePlayers() {
|
||||
const players = table.bootstrapTable("getSelections");
|
||||
players.map(function (player) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}`,
|
||||
data: JSON.stringify({
|
||||
action: "remove",
|
||||
playerId: {
|
||||
partyId: partyId,
|
||||
job: player.job,
|
||||
nick: player.nick,
|
||||
},
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addPlayerDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/jobs", jobInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
19
src/main/resources/html/redoc.html
Normal file
19
src/main/resources/html/redoc.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!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>
|
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>FFXIV loot tracker API</title>
|
||||
<!-- needed for adaptive design -->
|
||||
<meta charset="utf-8"/>
|
||||
<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">
|
||||
|
||||
<!--
|
||||
ReDoc doesn't change outer page styles
|
||||
-->
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
</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>
|
230
src/main/resources/html/users.html
Normal file
230
src/main/resources/html/users.html
Normal file
@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>User management</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<link href="/static/favicon.ico" rel="shortcut icon">
|
||||
|
||||
<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 href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.css" rel="stylesheet" type="text/css">
|
||||
|
||||
|
||||
<link href="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="/static/styles.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg justify-content-between align-items-center border-bottom">
|
||||
<a class="navbar-brand" id="navbar-title">Party</a>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-bis">best in slot</a>
|
||||
<a class="nav-item nav-link" id="navbar-loot">looted items</a>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<a class="nav-item nav-link" id="navbar-users">users</a>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div id="alert-placeholder" class="container"></div>
|
||||
|
||||
<div class="container">
|
||||
<h2>Users</h2>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar">
|
||||
<button id="add-btn" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#add-user-dialog" hidden>
|
||||
<i class="bi bi-plus"></i> add
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="reload()">
|
||||
<i class="bi bi-arrow-clockwise"></i> reload
|
||||
</button>
|
||||
<button id="remove-btn" class="btn btn-danger" onclick="removeUsers()" disabled hidden>
|
||||
<i class="bi bi-trash"></i> remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table id="users" class="table table-striped table-hover"
|
||||
data-click-to-select="true"
|
||||
data-export-options='{"fileName": "users"}'
|
||||
data-page-list="[25, 50, 100, all]"
|
||||
data-page-size="25"
|
||||
data-pagination="true"
|
||||
data-resizable="true"
|
||||
data-search="true"
|
||||
data-show-columns="true"
|
||||
data-show-columns-search="true"
|
||||
data-show-columns-toggle-all="true"
|
||||
data-show-export="true"
|
||||
data-show-fullscreen="true"
|
||||
data-show-search-clear-button="true"
|
||||
data-single-select="true"
|
||||
data-sortable="true"
|
||||
data-sort-reset="true"
|
||||
data-toolbar="#toolbar">
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th data-checkbox="true"></th>
|
||||
<th data-sortable="true" data-switchable="false" data-field="username">username</th>
|
||||
<th data-sortable="true" data-field="permission">permission</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="add-user-dialog" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">add new user</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
|
||||
<form class="modal-body">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="username">login</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="username" name="username" class="form-control" placeholder="username" onkeyup="disableAddUserForm()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="password">password</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="password" name="password" type="password" class="form-control" placeholder="password" onkeyup="disableAddUserForm()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-4 col-form-label" for="permission">permission</label>
|
||||
<div class="col-sm-8">
|
||||
<select id="permission" name="permission" class="form-control" title="permission"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="modal-footer">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<footer class="d-flex flex-wrap justify-content-between align-items-center border-top">
|
||||
<ul class="nav">
|
||||
<li><a class="nav-link" href="/" title="home">home</a></li>
|
||||
</ul>
|
||||
|
||||
<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/releases" title="releases list">releases</a></li>
|
||||
<li><a class="nav-link" href="https://github.com/arcan1s/ffxivbis/issues" title="issues tracker">report a bug</a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/tableexport.jquery.plugin/tableExport.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/bootstrap-table.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/export/bootstrap-table-export.min.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/bootstrap-table@1.19.1/dist/extensions/resizable/bootstrap-table-resizable.js"></script>
|
||||
|
||||
<script src="/static/utils.js"></script>
|
||||
<script src="/static/load.js"></script>
|
||||
|
||||
<script>
|
||||
const [partyId, isReadOnly] = getPartyId();
|
||||
const table = $("#users");
|
||||
const addButton = $("#add-btn");
|
||||
const removeButton = $("#remove-btn");
|
||||
|
||||
const addUserDialog = $("#add-user-dialog");
|
||||
const submitUserButton = $("#submit-btn");
|
||||
|
||||
const usernameInput = $("#username");
|
||||
const passwordInput = $("#password");
|
||||
const permissionInput = $("#permission");
|
||||
|
||||
function addUser() {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users`,
|
||||
data: JSON.stringify({
|
||||
partyId: partyId,
|
||||
username: usernameInput.val(),
|
||||
password: passwordInput.val(),
|
||||
permission: permissionInput.val(),
|
||||
}),
|
||||
type: "POST",
|
||||
contentType: "application/json",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function disableAddUserForm() {
|
||||
submitUserButton.attr("disabled", !(usernameInput.val() && passwordInput.val()));
|
||||
}
|
||||
|
||||
function hideControls() {
|
||||
addButton.attr("hidden", isReadOnly);
|
||||
removeButton.attr("hidden", isReadOnly);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users`,
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: function (data) {
|
||||
table.bootstrapTable("load", data);
|
||||
table.bootstrapTable("uncheckAll");
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
}
|
||||
|
||||
function removeUsers() {
|
||||
const users = table.bootstrapTable("getSelections");
|
||||
users.map(function (user) {
|
||||
$.ajax({
|
||||
url: `/api/v1/party/${partyId}/users/${user.username}`,
|
||||
type: "DELETE",
|
||||
success: function (_) { reload(); },
|
||||
error: function (jqXHR, _, errorThrown) { requestAlert(jqXHR, errorThrown); },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
setupFormClear(addUserDialog);
|
||||
setupRemoveButton(table, removeButton);
|
||||
|
||||
loadHeader(partyId);
|
||||
loadTypes("/api/v1/types/permissions", permissionInput);
|
||||
|
||||
hideControls();
|
||||
|
||||
table.bootstrapTable({});
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -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 {
|
||||
|
BIN
src/main/resources/static/favicon.ico
Normal file
BIN
src/main/resources/static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
54
src/main/resources/static/load.js
Normal file
54
src/main/resources/static/load.js
Normal file
@ -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);
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
44
src/main/resources/static/utils.js
Normal file
44
src/main/resources/static/utils.js
Normal file
@ -0,0 +1,44 @@
|
||||
function createAlert(message, placeholder) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = `<div class="alert alert-danger alert-dismissible" role="alert">${safe(message)}<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>`;
|
||||
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, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
@ -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`.
|
||||
|
@ -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
|
||||
|
23
src/main/scala/me/arcanis/ffxivbis/Configuration.scala
Normal file
23
src/main/scala/me/arcanis/ffxivbis/Configuration.scala
Normal file
@ -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"))
|
||||
)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
74
src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala
Normal file
74
src/main/scala/me/arcanis/ffxivbis/http/HttpLog.scala
Normal file
@ -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())
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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"),
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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)
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
@ -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]
|
||||
)
|
@ -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)
|
||||
}
|
@ -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
|
||||
)
|
@ -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
|
||||
)
|
@ -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)
|
||||
}
|
@ -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,
|
@ -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),
|
||||
)
|
||||
}
|
@ -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))
|
||||
}
|
@ -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}
|
@ -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}
|
@ -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}
|
@ -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}
|
@ -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
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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"))
|
||||
)
|
||||
)
|
||||
}
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
}
|
@ -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("")
|
||||
}
|
||||
}
|
@ -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")
|
||||
)
|
||||
}
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
}
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -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 =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
@ -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]) =
|
||||
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">" +
|
||||
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")
|
||||
)
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]]
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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")
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)"
|
||||
|
@ -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 =
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
@ -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 =>
|
||||
|
@ -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
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 =>
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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))))
|
||||
|
@ -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)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user