Registration, Login, Resume, Table Creation, Dark Style
This commit is contained in:
parent
b87dce55a8
commit
3a1ef01a54
337
Public/game.js
337
Public/game.js
@ -1,15 +1,270 @@
|
|||||||
|
|
||||||
|
// The web socket to connect to the server
|
||||||
|
var socket = null;
|
||||||
|
|
||||||
|
function closeSocketIfNeeded() {
|
||||||
|
if (socket) {
|
||||||
|
socket.close()
|
||||||
|
socket = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hideLoginWindow() {
|
function hideLoginWindow() {
|
||||||
document.getElementById("signup-window").style.display = "none"
|
document.getElementById("signup-window").style.display = "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showLoginWindow() {
|
||||||
|
document.getElementById("signup-window").style.display = "table"
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPlayerName(name) {
|
||||||
|
document.getElementById("player-name").innerHTML = name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayerName() {
|
||||||
|
return document.getElementById("player-name").innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginName() {
|
||||||
|
return document.getElementById("user-name").value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLoginName() {
|
||||||
|
document.getElementById("user-name").value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLoginPassword() {
|
||||||
|
return document.getElementById("user-pwd").value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLoginPassword() {
|
||||||
|
document.getElementById("user-pwd").value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionToken() {
|
||||||
|
return localStorage.getItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSessionToken(token) {
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSessionToken() {
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLoginError(text) {
|
||||||
|
document.getElementById("login-error").innerHTML = text
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableName() {
|
||||||
|
return document.getElementById("table-name-field").value
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearTableName() {
|
||||||
|
return document.getElementById("table-name-field").value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTableVisibility() {
|
||||||
|
return document.getElementById("table-public-checkbox").checked
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTableListContent(content) {
|
||||||
|
document.getElementById("table-list").innerHTML = content
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBlankLoginScreen(text) {
|
||||||
|
closeSocketIfNeeded()
|
||||||
|
clearLoginPassword()
|
||||||
|
clearLoginName()
|
||||||
|
deleteSessionToken()
|
||||||
|
showLoginWindow()
|
||||||
|
setLoginError(text)
|
||||||
|
}
|
||||||
|
|
||||||
async function registerUser() {
|
async function registerUser() {
|
||||||
let username = document.getElementById("user-name").value
|
console.log("Registration started")
|
||||||
let password = document.getElementById("user-pwd").value
|
performGetSessionTokenRequest("register")
|
||||||
errorField = document.getElementById("login-error")
|
}
|
||||||
|
|
||||||
console.log("Registration started");
|
async function deletePlayerAccount() {
|
||||||
|
const name = getPlayerName()
|
||||||
|
const password = getLoginPassword()
|
||||||
|
|
||||||
fetch("/create/user/" + username + "/" + password, { method: 'POST' })
|
fetch("/player/delete/" + username, { method: 'POST', body: password })
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.status == 200) { // Success
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.status == 400) { // Bad request
|
||||||
|
throw Error("The request had an error")
|
||||||
|
}
|
||||||
|
if (response.status == 401) { // Invalid session token
|
||||||
|
throw Error("Please log in again")
|
||||||
|
}
|
||||||
|
if (response.status == 403) { // Forbidden
|
||||||
|
throw Error("The password or name is incorrect")
|
||||||
|
}
|
||||||
|
if (response.status == 424) { // Failed dependency
|
||||||
|
throw Error("The request couldn't be completed")
|
||||||
|
}
|
||||||
|
throw Error("Unexpected registration response: " + response.statusText)
|
||||||
|
}).then(function() {
|
||||||
|
showBlankLoginScreen("")
|
||||||
|
console.log("Player deleted")
|
||||||
|
}).catch(function(error) {
|
||||||
|
closeSocketIfNeeded()
|
||||||
|
deleteSessionToken()
|
||||||
|
alert(error.message)
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loginUser() {
|
||||||
|
console.log("Login started");
|
||||||
|
performGetSessionTokenRequest("login")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logoutUser() {
|
||||||
|
const token = getSessionToken()
|
||||||
|
if (token) {
|
||||||
|
console.log("Logging out player")
|
||||||
|
performLogoutRequest(token)
|
||||||
|
} else {
|
||||||
|
console.log("No player to log out")
|
||||||
|
showBlankLoginScreen("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performLogoutRequest(token) {
|
||||||
|
fetch("/player/logout", { method: 'POST', body: token })
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.status == 200) { // Success
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (response.status == 400) { // Bad request
|
||||||
|
throw Error("The request had an error")
|
||||||
|
}
|
||||||
|
throw Error("Unexpected logout response: " + response.statusText)
|
||||||
|
}).then(function() {
|
||||||
|
showBlankLoginScreen("")
|
||||||
|
console.log("Player logged out")
|
||||||
|
}).catch(function(error) {
|
||||||
|
showBlankLoginScreen(error.message)
|
||||||
|
console.log(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertServerResponse(response) {
|
||||||
|
if (response.status == 200) { // Success
|
||||||
|
return response.text()
|
||||||
|
}
|
||||||
|
if (response.status == 400) { // Bad request
|
||||||
|
throw Error("The request was malformed")
|
||||||
|
}
|
||||||
|
if (response.status == 403) { // Forbidden
|
||||||
|
throw Error("Invalid username or password")
|
||||||
|
}
|
||||||
|
if (response.status == 406) { // notAcceptable
|
||||||
|
throw Error("The password or name is too long")
|
||||||
|
}
|
||||||
|
if (response.status == 409) { // Conflict
|
||||||
|
throw Error("A user with the same name is already registered")
|
||||||
|
}
|
||||||
|
if (response.status == 424) { // Failed dependency
|
||||||
|
throw Error("The request couldn't be completed")
|
||||||
|
}
|
||||||
|
throw Error("Unexpected response: " + response.statusText)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performGetSessionTokenRequest(type) {
|
||||||
|
const username = getLoginName()
|
||||||
|
const password = getLoginPassword()
|
||||||
|
|
||||||
|
console.log("Performing request " + type);
|
||||||
|
fetch("/player/" + type + "/" + username, { method: 'POST', body: password })
|
||||||
|
.then(convertServerResponse)
|
||||||
|
.then(function(token) {
|
||||||
|
setSessionToken(token)
|
||||||
|
setPlayerName(username)
|
||||||
|
hideLoginWindow()
|
||||||
|
setLoginError("")
|
||||||
|
console.log(type + " successful")
|
||||||
|
performGetPublicTablesList(token)
|
||||||
|
openSocket(token)
|
||||||
|
}).catch(function(error) {
|
||||||
|
setLoginError(error.message)
|
||||||
|
console.log(error)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExistingSession() {
|
||||||
|
const token = getSessionToken()
|
||||||
|
if (token) {
|
||||||
|
console.log("Resuming session with token " + token)
|
||||||
|
resumeSession(token)
|
||||||
|
} else {
|
||||||
|
console.log("No session to resume")
|
||||||
|
showLoginWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resumeSession(token) {
|
||||||
|
fetch("/player/resume", { method: 'POST', body: token })
|
||||||
|
.then(convertServerResponse)
|
||||||
|
.then(function(name) {
|
||||||
|
setPlayerName(name)
|
||||||
|
hideLoginWindow()
|
||||||
|
console.log("Session resumed")
|
||||||
|
performGetPublicTablesList(token)
|
||||||
|
openSocket(token)
|
||||||
|
}).catch(function(error) {
|
||||||
|
deleteSessionToken()
|
||||||
|
setLoginError(error.message)
|
||||||
|
showLoginWindow()
|
||||||
|
console.log(error)
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSocket(token) {
|
||||||
|
socket = new WebSocket(((window.location.protocol === "https:") ? "wss://" : "ws://") + window.location.host + "/session/start")
|
||||||
|
|
||||||
|
socket.onopen = function(e) {
|
||||||
|
socket.send(token);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = function(event) {
|
||||||
|
// TODO: Handle server data
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function(event) {
|
||||||
|
if (event.wasClean) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// e.g. server process killed or network down
|
||||||
|
// event.code is usually 1006 in this case
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = function(error) {
|
||||||
|
// error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTable() {
|
||||||
|
const tableName = getTableName()
|
||||||
|
const isVisible = getTableVisibility()
|
||||||
|
const token = getSessionToken()
|
||||||
|
if (token) {
|
||||||
|
performCreateTableRequest(token, tableName, isVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performCreateTableRequest(token, name, visibility) {
|
||||||
|
const vis = visibility ? "public" : "private";
|
||||||
|
fetch("/table/create/" + vis + "/" + name, { method: 'POST', body: token })
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
if (response.status == 200) { // Success
|
if (response.status == 200) { // Success
|
||||||
return response.text()
|
return response.text()
|
||||||
@ -17,36 +272,70 @@ async function registerUser() {
|
|||||||
if (response.status == 400) { // Bad request
|
if (response.status == 400) { // Bad request
|
||||||
throw Error("The request had an error")
|
throw Error("The request had an error")
|
||||||
}
|
}
|
||||||
if (response.status == 409) { // Conflict
|
if (response.status == 401) { // Token invalid
|
||||||
throw Error("A user with the same name is already registered")
|
showBlankLoginScreen("")
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
throw Error("Unexpected response: " + response.statusText)
|
throw Error("Unexpected registration response: " + response.statusText)
|
||||||
}).then(function(text) {
|
})
|
||||||
localStorage.setItem('token', text)
|
.then(function(tableId) {
|
||||||
hideLoginWindow()
|
if (tableId == "") {
|
||||||
console.log("Registered")
|
return;
|
||||||
|
}
|
||||||
|
clearTableName()
|
||||||
|
console.log("Created table " + tableId)
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
errorField.innerHTML = error.message
|
showBlankLoginScreen(error.message)
|
||||||
console.log(error)
|
console.log(error)
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadExistingSession() {
|
function refreshTables() {
|
||||||
console.log("Checking to resume session");
|
const token = getSessionToken()
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log("Resuming session with token " + token);
|
performGetPublicTablesList(token)
|
||||||
resumeSession(token);
|
} else {
|
||||||
|
showBlankLoginScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resumeSession(token) {
|
async function performGetPublicTablesList(token) {
|
||||||
|
fetch("/tables/public", { method: 'POST', body: token })
|
||||||
localStorage.removeItem('token');
|
.then(convertServerResponse)
|
||||||
hideLoginWindow()
|
.then(function(text) {
|
||||||
|
const decoded = atob(text)
|
||||||
|
const json = JSON.parse(decoded);
|
||||||
|
const html = processTableList(json)
|
||||||
|
setTableListContent(html)
|
||||||
|
}).catch(function(error) {
|
||||||
|
showBlankLoginScreen(error.message)
|
||||||
|
console.log(error)
|
||||||
|
return
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function loginUser() {
|
function processTableList(tables) {
|
||||||
|
var html = ""
|
||||||
|
for (let i = 0, len = tables.length, text = ""; i < len; i++) {
|
||||||
|
tableInfo = tables[i]
|
||||||
|
html += "<div class=\"table-row\">" +
|
||||||
|
"<button class=\"table-join-btn\" onclick=\"joinTable('" + tableInfo.id +
|
||||||
|
"')\">Join</button><div class=\"table-title\">" + tableInfo.name +
|
||||||
|
"</div><div class=\"table-subtitle\">Players: " + tableInfo.players.join(", ") + "</div></div>"
|
||||||
|
}
|
||||||
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function joinTable(tableId) {
|
||||||
|
const token = getSessionToken()
|
||||||
|
if (token) {
|
||||||
|
performJoinTableRequest(tableId, token)
|
||||||
|
} else {
|
||||||
|
showBlankLoginScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performJoinTableRequest(tableId, token) {
|
||||||
|
|
||||||
|
}
|
@ -7,17 +7,38 @@
|
|||||||
<link rel='stylesheet' type='text/css' media='screen' href='style.css'>
|
<link rel='stylesheet' type='text/css' media='screen' href='style.css'>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="signup-backdrop" id="signup-window">
|
<div id="top-bar">
|
||||||
<div class="signup-window-vertical">
|
<div id="player-info">
|
||||||
<div class="signup-window">
|
<div id="player-name"></div>
|
||||||
|
<button id="logout-button" class="standard-button" onclick="logoutUser()">Log out</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-list-bar">
|
||||||
|
<input type="text" id="table-name-field" name="tablename" placeholder="Create new table..." required>
|
||||||
|
<input type="checkbox" id="table-public-checkbox" name="public-table" checked="checked">
|
||||||
|
<span id="table-public-label">Public</span>
|
||||||
|
<button id="create-table-button" class="standard-button" onclick="createTable()">Create table</button>
|
||||||
|
<button id="refresh-tables" class="standard-button" onclick="refreshTables()">Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="table-list">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-list-window" id="table-window">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signup-window" id="signup-window">
|
||||||
|
<div class="signup-window-vertical-center">
|
||||||
|
<div class="signup-window-inner">
|
||||||
<label for="usrname">Username</label>
|
<label for="usrname">Username</label>
|
||||||
<input type="text" id="user-name" name="usrname" required>
|
<input type="text" id="user-name" name="usrname" required>
|
||||||
|
|
||||||
<label for="psw">Password</label>
|
<label for="psw">Password</label>
|
||||||
<input type="password" id="user-pwd" name="psw" required>
|
<input type="password" id="user-pwd" name="psw" required>
|
||||||
|
|
||||||
<button class="login-buttons" onclick="registerUser()">Register</button>
|
<button class="login-buttons standard-button" onclick="registerUser()">Register</button>
|
||||||
<button class="login-buttons" onclick="loginUser()">Log in</button>
|
<button class="login-buttons standard-button" onclick="loginUser()">Log in</button>
|
||||||
<div id="login-error"></div>
|
<div id="login-error"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
168
Public/style.css
168
Public/style.css
@ -1,25 +1,57 @@
|
|||||||
|
:root {
|
||||||
|
/* Color definitions for light mode */
|
||||||
|
--button-color: rgb(255, 172, 39);
|
||||||
|
--button-hover: rgb(255, 185, 72);
|
||||||
|
--button-text: rgb(0,0,0);
|
||||||
|
--standard-background: rgb(54, 54, 54);
|
||||||
|
--element-background: rgb(42, 42, 42);
|
||||||
|
--element-border: rgb(27, 27, 27);
|
||||||
|
--text-color: rgb(255,255,255);
|
||||||
|
}
|
||||||
|
|
||||||
/* Style all input fields */
|
/* Style all input fields */
|
||||||
|
|
||||||
|
.standard-button {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--button-color);
|
||||||
|
color: #000;
|
||||||
|
border-style: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.standard-button:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid var(--element-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background-color: var(--standard-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signup-window input {
|
||||||
|
width: 100%;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
html * {
|
html * {
|
||||||
font-family:-apple-system, BlinkMacSystemFont, "SF Hello", "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif;
|
font-family:-apple-system, BlinkMacSystemFont, "SF Hello", "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html {
|
body, html {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
background-color: var(--standard-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.signup-backdrop {
|
.signup-window {
|
||||||
background-color: #fff;
|
background-color: var(--standard-background);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -28,34 +60,142 @@ body, html {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signup-window-vertical {
|
.signup-window-vertical-center {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.signup-window {
|
.signup-window-inner {
|
||||||
background-color: #f1f1f1;
|
background-color: var(--element-background);
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: thin;
|
border-width: thin;
|
||||||
border-color: darkgray;
|
border-color: var(--element-border);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-buttons {
|
.login-buttons {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#top-bar {
|
||||||
|
height: 50px;
|
||||||
|
background-color: var(--element-background);
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-info {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
height: 50px;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
column-gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player-name {
|
||||||
|
text-align: right;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logout-button {
|
||||||
|
width: 80px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0px;
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-list-bar {
|
||||||
|
position: absolute;
|
||||||
|
width: 510px;
|
||||||
|
height: 40px;
|
||||||
|
top: 5px;
|
||||||
|
left: 10px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 200px 30px 50px 140px 90px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
justify-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-name-field {
|
||||||
|
width: 100%;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-public-checkbox {
|
||||||
|
grid-column: 2;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-public-label {
|
||||||
|
grid-column: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#create-table-button {
|
||||||
|
width: 120px;
|
||||||
|
grid-column: 4;
|
||||||
|
padding: 0px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-tables {
|
||||||
|
width: 90px;
|
||||||
|
grid-column: 5;
|
||||||
|
padding: 0px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#table-list {
|
||||||
|
margin: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-row {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #ccc;
|
margin-top: 10px;
|
||||||
color: #000;
|
background-color: var(--element-background);
|
||||||
border-style: hidden;
|
border-style: hidden;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-top: 5px;
|
font-size: medium;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
column-gap: 10px;
|
||||||
|
/* justify-items: left; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-join-btn {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100px;
|
||||||
|
background-color: var(--button-color);
|
||||||
|
color: var(--button-text);
|
||||||
|
border-style: hidden;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: medium;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-join-btn:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-buttons:hover {
|
.table-subtitle {
|
||||||
background-color: #ddd;
|
grid-column: 2;
|
||||||
}
|
grid-row: 2;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
14
Sources/App/Model/Crypto+Extensions.swift
Normal file
14
Sources/App/Model/Crypto+Extensions.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
import Crypto
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new access token.
|
||||||
|
*/
|
||||||
|
static func newToken() -> String {
|
||||||
|
Crypto.SymmetricKey.init(size: .bits128).withUnsafeBytes {
|
||||||
|
$0.hexEncodedString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,62 +1,68 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Crypto
|
import Vapor
|
||||||
|
|
||||||
let playerPerTable = 4
|
let playerPerTable = 4
|
||||||
|
|
||||||
|
typealias TableId = String
|
||||||
|
typealias TableName = String
|
||||||
|
|
||||||
final class Database {
|
final class Database {
|
||||||
|
|
||||||
/// A mapping between usernames and their password hashes
|
private let players: PlayerManagement
|
||||||
private var userPasswordHashes = [String: String]()
|
|
||||||
|
|
||||||
/// A mapping between usernames and generated access tokens for a session
|
private let tables: TableManagement
|
||||||
private var authTokenForUser = [String: String]()
|
|
||||||
|
|
||||||
/// A reverse mapping between generated access tokens and usernames
|
private var sessions: [SessionToken : WebSocket]
|
||||||
private var userForToken = [String: String]()
|
|
||||||
|
|
||||||
/// A list of table ids for public games
|
|
||||||
private var publicTables = Set<String>()
|
|
||||||
|
|
||||||
/// A mapping from table id to table name (for all tables)
|
|
||||||
private var tableNames = [String: String]()
|
|
||||||
|
|
||||||
/// A mapping from table id to participating players
|
|
||||||
private var tablePlayers = [String: [String]]()
|
|
||||||
|
|
||||||
/// A reverse list of players and their table id
|
|
||||||
private var playerTables = [String: String]()
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
self.players = PlayerManagement()
|
||||||
|
self.tables = TableManagement()
|
||||||
|
self.sessions = [:]
|
||||||
|
// TODO: Load server data from disk
|
||||||
|
// TODO: Save data to disk
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// MARK: Players & Sessions
|
||||||
Check if a user exists.
|
|
||||||
- Parameter name: The name of the user
|
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
|
||||||
- Returns: true, if the user exists
|
players.registerPlayer(named: name, hash: hash)
|
||||||
*/
|
|
||||||
func has(user: String) -> Bool {
|
|
||||||
userPasswordHashes[user] != nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
func passwordHashForExistingPlayer(named name: PlayerName) -> PasswordHash? {
|
||||||
Get the password hash for a user, if the user exists.
|
players.passwordHash(ofRegisteredPlayer: name)
|
||||||
- Parameter name: The name of the user
|
|
||||||
- Returns: The stored password hash, if the user exists
|
|
||||||
*/
|
|
||||||
func hash(ofUser name: String) -> String? {
|
|
||||||
userPasswordHashes[name]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
func deletePlayer(named name: PlayerName) {
|
||||||
Create a new user and assign an access token.
|
if let sessionToken = players.deletePlayer(named: name) {
|
||||||
- Parameter name: The name of the new user
|
closeAndRemoveSession(for: sessionToken)
|
||||||
- Parameter hash: The password hash of the user
|
}
|
||||||
- Returns: The generated access token for the session
|
// TODO: Delete player from tables
|
||||||
*/
|
}
|
||||||
func add(user name: String, hash: String) -> String {
|
|
||||||
self.userPasswordHashes[name] = hash
|
func isValid(sessionToken token: SessionToken) -> Bool {
|
||||||
return startSession(forUser: name)
|
players.isValid(sessionToken: token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSession(socket: WebSocket, sessionToken: SessionToken) {
|
||||||
|
closeAndRemoveSession(for: sessionToken)
|
||||||
|
sessions[sessionToken] = socket
|
||||||
|
socket.onText { [weak self] socket, text in
|
||||||
|
self?.didReceive(message: text, forSessionToken: sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didReceive(message: String, forSessionToken token: SessionToken) {
|
||||||
|
// TODO: Handle client requests
|
||||||
|
print("Session \(token.prefix(6)): \(message)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func endSession(forSessionToken token: SessionToken) {
|
||||||
|
players.endSession(forSessionToken: token)
|
||||||
|
closeAndRemoveSession(for: token)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closeAndRemoveSession(for token: SessionToken) {
|
||||||
|
_ = sessions.removeValue(forKey: token)?.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -64,32 +70,22 @@ final class Database {
|
|||||||
- Parameter name: The user name
|
- Parameter name: The user name
|
||||||
- Returns: The generated access token for the session
|
- Returns: The generated access token for the session
|
||||||
*/
|
*/
|
||||||
func startSession(forUser name: String) -> String {
|
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
|
||||||
let token = newToken()
|
players.startNewSessionForRegisteredPlayer(named: name)
|
||||||
self.authTokenForUser[name] = token
|
|
||||||
self.userForToken[token] = name
|
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
||||||
Get the user for a session token.
|
players.registeredPlayerExists(withSessionToken: token)
|
||||||
- Parameter token: The access token for the user
|
|
||||||
- Returns: The name of the user, if it exists
|
|
||||||
*/
|
|
||||||
func user(forToken token: String) -> String? {
|
|
||||||
userForToken[token]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableExists(named name: String) -> Bool {
|
// MARK: Tables
|
||||||
tableNames.contains { $0.value == name }
|
|
||||||
|
func tableExists(withId id: TableId) -> Bool {
|
||||||
|
tables.tableExists(withId: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tableExists(withId id: String) -> Bool {
|
func tableIsFull(withId id: TableId) -> Bool {
|
||||||
tableNames[id] != nil
|
tables.tableIsFull(withId: id)
|
||||||
}
|
|
||||||
|
|
||||||
func tableIsFull(withId id: String) -> Bool {
|
|
||||||
tablePlayers[id]!.count < playerPerTable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,43 +95,22 @@ final class Database {
|
|||||||
- Parameter visible: Indicates that this is a game joinable by everyone
|
- Parameter visible: Indicates that this is a game joinable by everyone
|
||||||
- Returns: The table id
|
- Returns: The table id
|
||||||
*/
|
*/
|
||||||
func createTable(named name: String, player: String, visible: Bool) -> String {
|
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
|
||||||
let tableId = newToken()
|
tables.createTable(named: name, player: player, visible: visible)
|
||||||
|
|
||||||
tableNames[tableId] = tableId
|
|
||||||
tablePlayers[tableId] = [player]
|
|
||||||
playerTables[player] = tableId
|
|
||||||
|
|
||||||
if visible {
|
|
||||||
publicTables.insert(tableId)
|
|
||||||
}
|
|
||||||
return tableId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublicTableInfos() -> [TableInfo] {
|
func getPublicTableInfos() -> [TableInfo] {
|
||||||
publicTables.map { tableId in
|
tables.getPublicTableInfos()
|
||||||
TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!)
|
|
||||||
}.sorted()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func join(tableId: String, player: String) {
|
func join(tableId: TableId, player: PlayerName) {
|
||||||
tablePlayers[tableId]!.append(player)
|
let playersAtTable = tables.join(tableId: tableId, player: player)
|
||||||
if let oldTable = playerTables[tableId] {
|
playersAtTable
|
||||||
remove(player: player, fromTable: oldTable)
|
.compactMap { players.sessionToken(forPlayer: $0) } // Session Tokens
|
||||||
}
|
.compactMap { sessions[$0] } // Sockets
|
||||||
playerTables[tableId] = tableId
|
.forEach { socket in
|
||||||
}
|
// TODO: Notify sessions about changed players
|
||||||
|
// socket.send("")
|
||||||
func remove(player: String, fromTable tableId: String) {
|
}
|
||||||
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Create a new access token.
|
|
||||||
*/
|
|
||||||
private func newToken() -> String {
|
|
||||||
Crypto.SymmetricKey.init(size: .bits128).withUnsafeBytes {
|
|
||||||
$0.hexEncodedString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
106
Sources/App/Model/PlayerManagement.swift
Normal file
106
Sources/App/Model/PlayerManagement.swift
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
typealias PlayerName = String
|
||||||
|
typealias PasswordHash = String
|
||||||
|
typealias SessionToken = String
|
||||||
|
|
||||||
|
/// Manages player registration, session tokens and password hashes
|
||||||
|
final class PlayerManagement {
|
||||||
|
|
||||||
|
/// A mapping between player name and their password hashes
|
||||||
|
private var playerPasswordHashes = [PlayerName: PasswordHash]()
|
||||||
|
|
||||||
|
/// A mapping between player name and generated access tokens for a session
|
||||||
|
private var sessionTokenForPlayer = [PlayerName: SessionToken]()
|
||||||
|
|
||||||
|
/// A reverse mapping between generated access tokens and player name
|
||||||
|
private var playerNameForToken = [SessionToken: PlayerName]()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check if a player exists.
|
||||||
|
- Parameter name: The name of the player
|
||||||
|
- Returns: true, if the player exists
|
||||||
|
*/
|
||||||
|
func hasRegisteredPlayer(named user: PlayerName) -> Bool {
|
||||||
|
playerPasswordHashes[user] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Get the password hash for a player, if the player exists.
|
||||||
|
- Parameter name: The name of the player
|
||||||
|
- Returns: The stored password hash, if the player exists
|
||||||
|
*/
|
||||||
|
func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? {
|
||||||
|
playerPasswordHashes[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new player and assign an access token.
|
||||||
|
- Parameter name: The name of the new player
|
||||||
|
- Parameter hash: The password hash of the player
|
||||||
|
- Returns: The generated access token for the session
|
||||||
|
*/
|
||||||
|
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
|
||||||
|
guard !hasRegisteredPlayer(named: name) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.playerPasswordHashes[name] = hash
|
||||||
|
return startNewSessionForRegisteredPlayer(named: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Delete a player
|
||||||
|
- Parameter name: The name of the player to delete.
|
||||||
|
- Returns: The session token of the current player, if one exists
|
||||||
|
*/
|
||||||
|
func deletePlayer(named name: PlayerName) -> SessionToken? {
|
||||||
|
playerPasswordHashes.removeValue(forKey: name)
|
||||||
|
guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
playerNameForToken.removeValue(forKey: sessionToken)
|
||||||
|
return sessionToken
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValid(sessionToken token: SessionToken) -> Bool {
|
||||||
|
playerNameForToken[token] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionToken(forPlayer player: PlayerName) -> SessionToken? {
|
||||||
|
sessionTokenForPlayer[player]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Start a new session for an existing player.
|
||||||
|
- Parameter name: The player name
|
||||||
|
- Returns: The generated access token for the session
|
||||||
|
*/
|
||||||
|
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
|
||||||
|
let token = SessionToken.newToken()
|
||||||
|
self.sessionTokenForPlayer[name] = token
|
||||||
|
self.playerNameForToken[token] = name
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
func endSession(forSessionToken token: SessionToken) {
|
||||||
|
guard let player = playerNameForToken.removeValue(forKey: token) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionTokenForPlayer.removeValue(forKey: player)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Get the player for a session token.
|
||||||
|
- Parameter token: The access token for the player
|
||||||
|
- Returns: The name of the player, if it exists
|
||||||
|
*/
|
||||||
|
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
||||||
|
playerNameForToken[token]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
75
Sources/App/Model/TableManagement.swift
Normal file
75
Sources/App/Model/TableManagement.swift
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class TableManagement {
|
||||||
|
|
||||||
|
/// A list of table ids for public games
|
||||||
|
private var publicTables = Set<TableId>()
|
||||||
|
|
||||||
|
/// A mapping from table id to table name (for all tables)
|
||||||
|
private var tableNames = [TableId: TableName]()
|
||||||
|
|
||||||
|
/// A mapping from table id to participating players
|
||||||
|
private var tablePlayers = [TableId: [PlayerName]]()
|
||||||
|
|
||||||
|
/// A reverse list of players and their table id
|
||||||
|
private var playerTables = [PlayerName: TableId]()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableExists(withId id: TableId) -> Bool {
|
||||||
|
tableNames[id] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableIsFull(withId id: TableId) -> Bool {
|
||||||
|
(tablePlayers[id]?.count ?? playerPerTable) < playerPerTable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new table with optional players.
|
||||||
|
- Parameter name: The name of the table
|
||||||
|
- Parameter players: The player creating the table
|
||||||
|
- Parameter visible: Indicates that this is a game joinable by everyone
|
||||||
|
- Returns: The table id
|
||||||
|
*/
|
||||||
|
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
|
||||||
|
let tableId = TableId.newToken()
|
||||||
|
|
||||||
|
tableNames[tableId] = name
|
||||||
|
tablePlayers[tableId] = [player]
|
||||||
|
playerTables[player] = tableId
|
||||||
|
|
||||||
|
if visible {
|
||||||
|
publicTables.insert(tableId)
|
||||||
|
}
|
||||||
|
return tableId
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPublicTableInfos() -> [TableInfo] {
|
||||||
|
publicTables.map { tableId in
|
||||||
|
TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!)
|
||||||
|
}.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Join a table.
|
||||||
|
- Returns: The player names present at the table
|
||||||
|
*/
|
||||||
|
func join(tableId: TableId, player: PlayerName) -> [PlayerName] {
|
||||||
|
guard var players = tablePlayers[tableId] else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
players.append(player)
|
||||||
|
if let oldTable = playerTables[tableId] {
|
||||||
|
remove(player: player, fromTable: oldTable)
|
||||||
|
}
|
||||||
|
tablePlayers[tableId] = players
|
||||||
|
playerTables[tableId] = tableId
|
||||||
|
return players
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(player: PlayerName, fromTable tableId: TableId) {
|
||||||
|
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
|
||||||
|
}
|
||||||
|
}
|
@ -1,99 +1,230 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
|
/// The JSON encoder for responses
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
|
/// The maximum length of a valid player name
|
||||||
|
private let maximumPlayerNameLength = 40
|
||||||
|
|
||||||
|
/// The maximum length of a valid password
|
||||||
|
private let maximumPasswordLength = 40
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
app.get { req in
|
|
||||||
return "It works!"
|
|
||||||
}
|
|
||||||
|
|
||||||
app.post("create", "user", ":name", ":hash") { req -> String in
|
// MARK: Players & Sessions
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new player
|
||||||
|
- Parameter name: The name of the player, included in the url
|
||||||
|
- Parameter password: The password of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing name or password
|
||||||
|
- 406: Password or name too long
|
||||||
|
- 409: A player with the same name already exists
|
||||||
|
- 424: The password could not be hashed
|
||||||
|
- Returns: The session token for the registered user
|
||||||
|
*/
|
||||||
|
app.post("player", "register", ":name") { req -> String in
|
||||||
guard let name = req.parameters.get("name"),
|
guard let name = req.parameters.get("name"),
|
||||||
let hash = req.parameters.get("hash") else {
|
let password = req.body.string else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
let digest = try req.password.hash(hash)
|
guard name.count < maximumPlayerNameLength,
|
||||||
|
password.count < maximumPasswordLength else {
|
||||||
|
throw Abort(.notAcceptable) // 406
|
||||||
|
}
|
||||||
|
|
||||||
guard !database.has(user: name) else {
|
guard let hash = try? req.password.hash(password) else {
|
||||||
throw Abort(.conflict)
|
throw Abort(.failedDependency) // 424
|
||||||
|
}
|
||||||
|
guard let token = database.registerPlayer(named: name, hash: hash) else {
|
||||||
|
throw Abort(.conflict) // 409
|
||||||
}
|
}
|
||||||
let token = database.add(user: name, hash: digest)
|
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("create", "session", ":name", ":hash") { req -> String in
|
/**
|
||||||
|
Delete a player.
|
||||||
|
- Parameter name: The name of the player, included in the url
|
||||||
|
- Parameter password: The password of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing name or password
|
||||||
|
- 403: The password or user name is invalid
|
||||||
|
- 424: The password could not be hashed
|
||||||
|
- Returns: Nothing
|
||||||
|
*/
|
||||||
|
app.post("player", "delete", ":name") { req -> String in
|
||||||
guard let name = req.parameters.get("name"),
|
guard let name = req.parameters.get("name"),
|
||||||
let hash = req.parameters.get("hash") else {
|
let password = req.body.string else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest) // 400
|
||||||
|
}
|
||||||
|
guard let hash = database.passwordHashForExistingPlayer(named: name) else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
}
|
}
|
||||||
guard let digest = database.hash(ofUser: name),
|
guard let isValid = try? req.password.verify(password, created: hash) else {
|
||||||
try req.password.verify(hash, created: digest) else {
|
throw Abort(.failedDependency) // 424
|
||||||
throw Abort(.forbidden)
|
|
||||||
}
|
}
|
||||||
let token = database.startSession(forUser: name)
|
guard isValid else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
|
}
|
||||||
|
database.deletePlayer(named: name)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Log in as an existing player.
|
||||||
|
- Parameter name: The name of the player, included in the url
|
||||||
|
- Parameter password: The password of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing name or password
|
||||||
|
- 403: The password or user name is invalid
|
||||||
|
- 424: The password could not be hashed
|
||||||
|
- Returns: The session token for the user
|
||||||
|
*/
|
||||||
|
app.post("player", "login", ":name") { req -> String in
|
||||||
|
guard let name = req.parameters.get("name"),
|
||||||
|
let password = req.body.string else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
|
}
|
||||||
|
guard let hash = database.passwordHashForExistingPlayer(named: name) else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
|
}
|
||||||
|
guard let isValid = try? req.password.verify(password, created: hash) else {
|
||||||
|
throw Abort(.failedDependency) // 424
|
||||||
|
}
|
||||||
|
guard isValid else {
|
||||||
|
throw Abort(.forbidden) // 403
|
||||||
|
}
|
||||||
|
let token = database.startNewSessionForRegisteredPlayer(named: name)
|
||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("session", "resume", ":token") { req -> String in
|
/**
|
||||||
guard let token = req.parameters.get("token") else {
|
Log in using a session token.
|
||||||
throw Abort(.badRequest)
|
- Parameter token: The session token of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing token
|
||||||
|
- 401: The token is invalid
|
||||||
|
- Returns: The player name associated with the session token
|
||||||
|
*/
|
||||||
|
app.post("player", "resume") { req -> String in
|
||||||
|
guard let token = req.body.string else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let user = database.user(forToken: token) else {
|
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.forbidden)
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
return user
|
return player
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Improve token handling (it will be logged when included in url!)
|
/**
|
||||||
app.get("create", "table", ":visibility", ":name", ":token") { req -> String in
|
Log out.
|
||||||
guard let name = req.parameters.get("name"),
|
- Parameter name: The name of the player, included in the url
|
||||||
let token = req.parameters.get("token"),
|
- Parameter token: The session token of the player, as a string in the request body
|
||||||
let visibility = req.parameters.get("visibility") else {
|
- Throws:
|
||||||
throw Abort(.badRequest)
|
- 400: Missing token
|
||||||
|
- Returns: Nothing
|
||||||
|
- Note: The request always succeeds when correctly formed, even for invalid and expired tokens
|
||||||
|
*/
|
||||||
|
app.post("player", "logout") { req -> String in
|
||||||
|
guard let token = req.body.string else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
|
database.endSession(forSessionToken: token)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Start a new bidirectional session connection.
|
||||||
|
- Returns: Nothing
|
||||||
|
- Note: The first message over the connection must be a valid session token.
|
||||||
|
*/
|
||||||
|
app.webSocket("session", "start") { req, socket in
|
||||||
|
socket.onText { socket, text in
|
||||||
|
guard database.isValid(sessionToken: text) else {
|
||||||
|
_ = socket.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
database.startSession(socket: socket, sessionToken: text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Tables
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new table.
|
||||||
|
- Parameter visibility: Indicate a `"public"` or `"private"` table
|
||||||
|
- Parameter token: The session token of the player, as a string in the request body
|
||||||
|
- Returns: The table id
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing token, table name or invalid visibility
|
||||||
|
- 401: The session token is invalid
|
||||||
|
*/
|
||||||
|
app.post("table", "create", ":visibility", ":name") { req -> String in
|
||||||
|
guard let visibility = req.parameters.get("visibility"),
|
||||||
|
let tableName = req.parameters.get("name"),
|
||||||
|
let token = req.body.string else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
|
}
|
||||||
let isVisible: Bool
|
let isVisible: Bool
|
||||||
if visibility == "private" {
|
if visibility == "private" {
|
||||||
isVisible = false
|
isVisible = false
|
||||||
} else if visibility == "public" {
|
} else if visibility == "public" {
|
||||||
isVisible = true
|
isVisible = true
|
||||||
} else {
|
} else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let user = database.user(forToken: token) else {
|
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.forbidden)
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
guard !database.tableExists(named: name) else {
|
let tableId = database.createTable(named: tableName, player: player, visible: isVisible)
|
||||||
throw Abort(.conflict)
|
|
||||||
}
|
|
||||||
let tableId = database.createTable(named: name, player: user, visible: isVisible)
|
|
||||||
return tableId
|
return tableId
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("tables", "public", ":token") { req -> String in
|
/**
|
||||||
guard let token = req.parameters.get("token") else {
|
List the public tables.
|
||||||
throw Abort(.badRequest)
|
- Parameter token: The session token of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing token
|
||||||
|
- 403: The session token is invalid
|
||||||
|
- Returns: A JSON object with a list of public tables (id, name, player list)
|
||||||
|
*/
|
||||||
|
app.post("tables", "public") { req -> String in
|
||||||
|
guard let token = req.body.string else {
|
||||||
|
throw Abort(.badRequest) // 400
|
||||||
}
|
}
|
||||||
guard let _ = database.user(forToken: token) else {
|
guard database.isValid(sessionToken: token) else {
|
||||||
throw Abort(.forbidden)
|
throw Abort(.forbidden) // 403
|
||||||
}
|
}
|
||||||
let list = database.getPublicTableInfos()
|
let list = database.getPublicTableInfos()
|
||||||
return try encoder.encode(list).base64EncodedString()
|
return try encoder.encode(list).base64EncodedString()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post("table", "join", ":table", ":token") { req -> String in
|
/**
|
||||||
|
Join a table.
|
||||||
|
- Parameter table: The table id
|
||||||
|
- Parameter token: The session token of the player, as a string in the request body
|
||||||
|
- Throws:
|
||||||
|
- 400: Missing token
|
||||||
|
- 401: The session token is invalid
|
||||||
|
- 404: The table id doesn't exist
|
||||||
|
- 406: The table is already full and can't be joined
|
||||||
|
- Returns: Nothing
|
||||||
|
*/
|
||||||
|
app.post("table", "join", ":table") { req -> String in
|
||||||
guard let table = req.parameters.get("table"),
|
guard let table = req.parameters.get("table"),
|
||||||
let token = req.parameters.get("token") else {
|
let token = req.body.string else {
|
||||||
throw Abort(.badRequest)
|
throw Abort(.badRequest)
|
||||||
}
|
}
|
||||||
guard let player = database.user(forToken: token) else {
|
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
|
||||||
throw Abort(.forbidden)
|
throw Abort(.unauthorized) // 401
|
||||||
}
|
}
|
||||||
guard database.tableExists(withId: table) else {
|
guard database.tableExists(withId: table) else {
|
||||||
throw Abort(.notFound)
|
throw Abort(.notFound) // 404
|
||||||
}
|
}
|
||||||
guard !database.tableIsFull(withId: table) else {
|
guard !database.tableIsFull(withId: table) else {
|
||||||
throw Abort(.notAcceptable)
|
throw Abort(.notAcceptable) // 406
|
||||||
}
|
}
|
||||||
database.join(tableId: table, player: player)
|
database.join(tableId: table, player: player)
|
||||||
return ""
|
return ""
|
||||||
|
Loading…
Reference in New Issue
Block a user