Extract login pages to own files

Fix api

Remove login window
This commit is contained in:
Christoph Hagen 2022-10-12 19:55:22 +02:00
parent fe429ea7d5
commit 213bb1c179
12 changed files with 459 additions and 135 deletions

View File

@ -6,13 +6,25 @@ const apiPath = "/schafkopf"
var useEnglishTexts = false var useEnglishTexts = false
const headerKeyPassword = "password";
const headerKeyToken = "token";
const headerKeyName = "name";
const headerKeyMail = "email";
function webSocketPath() { function webSocketPath() {
const prefix = (window.location.protocol === "https:") ? "wss://" : "ws://" const prefix = (window.location.protocol === "https:") ? "wss://" : "ws://"
return prefix + window.location.host + apiPath + "/session/start" return prefix + window.location.host + apiPath + "/session/start"
} }
async function performRegisterPlayerRequest(name, password) { async function performRegisterPlayerRequest(name, password, email) {
return fetch(apiPath + "/player/register/" + name, { method: 'POST', body: password }) return fetch(apiPath + "/player/register", {
method: 'POST',
headers: {
[headerKeyName]: name,
[headerKeyPassword]: password,
[headerKeyMail]: email
}
})
.then(convertServerResponse) .then(convertServerResponse)
} }
@ -44,6 +56,22 @@ async function performGetCurrentTableRequest(token) {
.then(convertJsonResponse) .then(convertJsonResponse)
} }
async function performRecoveryEmailRequest(name) {
return fetch(apiPath + "/player/password/reset", {
method: 'POST',
headers: { [headerKeyName] : name },
})
.then(convertServerResponse)
}
async function performResetPasswordRequest(token, password) {
return fetch(apiPath + "/player/reset", {
method: 'POST',
headers: { [headerKeyPassword] : password, [headerKeyToken] : token }
})
.then(convertServerResponse)
}
async function performCreateTableRequest(token, name, visibility) { async function performCreateTableRequest(token, name, visibility) {
const vis = visibility ? "public" : "private"; const vis = visibility ? "public" : "private";
return fetch(apiPath + "/table/create/" + vis + "/" + name, { method: 'POST', body: token }) return fetch(apiPath + "/table/create/" + vis + "/" + name, { method: 'POST', body: token })
@ -160,4 +188,4 @@ function convertStateToString(state) {
default: default:
return state return state
} }
} }

View File

@ -4,14 +4,11 @@
var playerName = "" var playerName = ""
var debugSessionToken = null var debugSessionToken = null
const debugMode = false // Does not load session token, to allow multiple players per browser
const offlineText = "Offline" const offlineText = "Offline"
const missingPlayerText = "Leer" const missingPlayerText = "Leer"
const elementIdUserName = "user-name"
const elementIdUserPssword = "user-pwd"
const elementIdPlayerCards = "player-cards" const elementIdPlayerCards = "player-cards"
const elementIdLoginWindow = "login-window" const elementIdLoginWindow = "login-window"
const elementIdTopBar = "top-bar" const elementIdTopBar = "top-bar"
@ -28,22 +25,6 @@ const elementIdActionBar = "action-bar"
const elementIdAvailableGamesList = "available-games-list" const elementIdAvailableGamesList = "available-games-list"
const elementIdGameSummary = "game-summary" const elementIdGameSummary = "game-summary"
const localStorageTokenId = "token"
function showDebugLogins() {
document.getElementById("login-window-inner").innerHTML +=
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('a')\">Player A</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('b')\">Player B</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('c')\">Player C</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('d')\">Player D</button>"
}
function loginDebugUser(name) {
document.getElementById(elementIdUserName).value = name
document.getElementById(elementIdUserPssword).value = name
loginUser()
}
function setDisplayStyle(id, style) { function setDisplayStyle(id, style) {
document.getElementById(id).style.display = style document.getElementById(id).style.display = style
} }
@ -102,41 +83,6 @@ function setPlayerName(name) {
playerName = name playerName = name
} }
function getLoginName() {
return document.getElementById(elementIdUserName).value
}
function clearLoginName() {
document.getElementById(elementIdUserName).value = ""
}
function getLoginPassword() {
return document.getElementById(elementIdUserPssword).value
}
function clearLoginPassword() {
document.getElementById(elementIdUserPssword).value = ""
}
function getSessionToken() {
if (debugMode) {
return debugSessionToken
}
return localStorage.getItem(localStorageTokenId)
}
function setSessionToken(token) {
if (debugMode) {
debugSessionToken = token
return
}
localStorage.setItem(localStorageTokenId, token)
}
function deleteSessionToken() {
localStorage.removeItem(localStorageTokenId)
}
function setLoginError(text) { function setLoginError(text) {
document.getElementById(elementIdLoginError).innerHTML = text document.getElementById(elementIdLoginError).innerHTML = text
} }
@ -388,8 +334,8 @@ function textForAction(action) {
return "Herz Solo" return "Herz Solo"
case "solo-schelln": case "solo-schelln":
return "Schelln Solo" return "Schelln Solo"
} }
return action return action
} }
function performAction(action) { function performAction(action) {
@ -411,4 +357,4 @@ function showAvailableGames(games) {
html += "<div class=\"standard-button available-game\">" + content + "</div>" html += "<div class=\"standard-button available-game\">" + content + "</div>"
} }
document.getElementById(elementIdAvailableGamesList).innerHTML = html document.getElementById(elementIdAvailableGamesList).innerHTML = html
} }

View File

@ -119,19 +119,20 @@ function deletePlayerAccount() {
} }
function loadExistingSession() { function loadExistingSession() {
if (debugMode) { const token = loadSessionToken()
showDebugLogins()
}
const token = getSessionToken()
if (token == null) { if (token == null) {
showBlankLogin() window.location.href = "login.html";
return return
} }
resumeSessionRequest(token) resumeSessionRequest(token)
.then(function(name) { .then(function(name) {
setPlayerName(name) setPlayerName(name)
loadCurrentTable(token) loadCurrentTable(token)
}).catch(showLoginWithError) }).catch(function(error) {
console.log("Failed to resume session");
console.log(error);
window.location.href = "login.html";
})
} }
function loadCurrentTable(token) { function loadCurrentTable(token) {
@ -265,4 +266,4 @@ function playCard(card) {
.catch(function(error) { .catch(function(error) {
console.log(error) console.log(error)
}) })
} }

69
Public/login.css Normal file
View File

@ -0,0 +1,69 @@
#sheephead-logo {
color: rgb(0, 255, 0);
font-family: monospace, monospace;
white-space: pre;
text-align: center;
margin-bottom: 20px;
}
#login-window input {
width: 100%;
margin-top: 6px;
margin-bottom: 16px;
}
#login-window {
background-color: var(--standard-background);
height: 100%;
width: 100%;
top: 0;
left: 0;
display: table;
position: absolute;
}
#login-window-vertical-center {
display: table-cell;
vertical-align: middle;
}
#login-window-inner {
background-color: var(--element-background);
margin-left: auto;
margin-right: auto;
width: 300px;
border-radius: 10px;
border-style: solid;
border-width: thin;
border-color: var(--element-border);
padding: 10px;
}
.login-buttons {
width: 100%;
margin-top: 5px;
}
#login-error {
margin-top: 8px;
}
label a {
color: var(--button-color);
margin-left: auto;
}
label {
display: flex;
width: 100%;
}
#user-hint {
padding-top: 5px;
}
#field-note {
padding-left: 5px;
color: gray;
}

70
Public/login.html Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Schafkopf</title>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel='stylesheet' type='text/css' media='screen' href='style.css'/>
<link rel='stylesheet' type='text/css' media='screen' href='login.css'/>
<script src='api.js'></script>
<script src='storage.js'></script>
<script src='login.js'></script>
</head>
<body>
<div id="login-window">
<div id="login-window-vertical-center">
<div id="login-window-inner">
<div id="sheephead-logo">\_______/<br>\ - - /<br>\ | /<br>\_/<br><br>Sheephead</div>
<label for="usrname">Username<a href="register.html">Create account</a></label>
<input type="text" id="user-name" name="usrname" required/>
<label for="psw">Password<a href="reset.html">Reset password</a></label>
<input type="password" id="user-pwd" name="psw" required/>
<button class="login-buttons standard-button" onclick="loginUser()">Log in</button>
<div id="user-hint"></div>
</div>
</div>
</div>
<script>
function loginUser() {
const username = getLoginName();
const password = getLoginPassword();
if (username == "") {
setTextHint("Please enter your user name");
return
}
if (password == "") {
setTextHint("Please enter a password");
return
}
performLoginPlayerRequest(username, password)
.then(function(token) {
storePlayerNameAndToken(username, token)
window.location.href = "schafkopf.html";
}).catch(function(error) {
console.log("Failed to log in")
console.log(error)
// TODO: Create better error message for user
setTextHint(error);
})
}
function showDebugLogins() {
document.getElementById("login-window-inner").innerHTML +=
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('a')\">Player A</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('b')\">Player B</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('c')\">Player C</button>" +
"<button class=\"login-buttons standard-button\" onclick=\"loginDebugUser('d')\">Player D</button>"
}
function loginDebugUser(name) {
setLoginName(name)
setLoginPassword(name)
loginUser()
}
loadExistingSession()
</script>
</body>
</html>

64
Public/login.js Normal file
View File

@ -0,0 +1,64 @@
// Site-specific elements
const elementIdUserName = "user-name";
const elementIdUserPassword = "user-pwd";
const elementIdUserEmail = "user-email";
const elementIdUserHint = "user-hint";
function getLoginName() {
return document.getElementById(elementIdUserName).value;
}
function clearLoginName() {
document.getElementById(elementIdUserName).value = "";
}
function setLoginName(name) {
document.getElementById(elementIdUserName).value = name;
}
function getLoginPassword() {
return document.getElementById(elementIdUserPassword).value;
}
function clearLoginPassword() {
document.getElementById(elementIdUserPassword).value = "";
}
function setLoginPassword(password) {
document.getElementById(elementIdUserPassword).value = password;
}
function getLoginEmail() {
return document.getElementById(elementIdUserEmail).value;
}
/*
* Set a text to indicate an action, warning or other message below the registration field;
*/
function setTextHint(text) {
document.getElementById(elementIdUserHint).innerHTML = text;
}
/*
* Attempt to reopen an existing session, either from the login or register page.
*
* It could be argued that this shouldn't be attempted for the registration page,
* but, a user may accidentally go to the registration page through the browser history,
* and then it will be convenient to be redirected to the game.
*/
function loadExistingSession() {
const token = loadSessionToken();
if (token == null) {
console.log("No session token to load")
return;
}
resumeSessionRequest(token)
.then(function(name) {
storePlayerNameAndToken(name, token);
window.location.href = "list.html";
}).catch(function(error) {
// We don't expect a session to resume if the registration page is shown,
// and if the token is expired on the login page we just fail quietly.
console.log(error);
})
}

56
Public/recovery.html Normal file
View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Schafkopf</title>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel='stylesheet' type='text/css' href='style.css'/>
<link rel='stylesheet' type='text/css' href='login.css'/>
<script src='api.js'></script>
<script src='login.js'></script>
</head>
<body>
<div id="login-window">
<div id="login-window-vertical-center">
<div id="login-window-inner">
<div id="sheephead-logo">\_______/<br>\ - - /<br>\ | /<br>\_/<br><br>Sheephead</div>
<label for="psw">New Password</label>
<input type="password" id="user-pwd" name="psw" required>
<button class="login-buttons standard-button" onclick="resetPassword()">Set new password</button>
<div id="user-hint"></div>
</div>
</div>
</div>
<script>
function resetPassword() {
const password = getLoginPassword()
if (password == "") {
setTextHint("Please enter a password")
return
}
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
if (!urlParams.has('token')) {
setTextHint("Please open a valid recovery link")
return
}
const token = urlParams.get('token');
if (token == "") {
setTextHint("Please open a valid recovery link")
return
}
performResetPasswordRequest(token, password)
.then(function(value) {
window.location.href = "login.html"
})
.catch(setTextHint)
}
</script>
</body>
</html>

75
Public/register.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Schafkopf</title>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel='stylesheet' type='text/css' media='screen' href='style.css'/>
<link rel='stylesheet' type='text/css' href='login.css'/>
<script src='api.js'></script>
<script src='storage.js'></script>
<script src='login.js'></script>
</head>
<body id="login-window">
<div id="login-window-vertical-center">
<div id="login-window-inner">
<div id="sheephead-logo">\_______/<br>\ - - /<br>\ | /<br>\_/<br><br>Sheephead</div>
<label for="usrname">Username<span id="field-note">Required</span><a href="login.html">Log in instead</a></label>
<input type="text" id="user-name" name="usrname" required autofocus/>
<label for="psw">Password<span id="field-note">Required</span></label>
<input type="password" id="user-pwd" name="psw" required/>
<label for="email">Email<span id="field-note">Optional, for password reset</span></label>
<input type="email" id="user-email" name="email"/>
<button class="login-buttons standard-button" onclick="registerUser()">Create account</button>
<div id="user-hint"></div>
</div>
</div>
<script>
// Email address validation regex
const mailformat = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
/*
* Register a new account.
*
* The function extracts the fields from the form, and attempts to create the account.
* Invalid emails are checked using basic validation.
*
* In case the registration fails, then we show an error to the user.
*/
function registerUser() {
const username = getLoginName();
if (username == "") {
setTextHint("Please enter your user name");
return;
}
const password = getLoginPassword();
if (password == "") {
setTextHint("Please enter a password");
return;
}
const email = getLoginEmail();
if (email != "" && !email.match(mailformat)) {
setTextHint("Invalid email address");
return;
}
performRegisterPlayerRequest(username, password, email)
.then(function(token) {
storePlayerNameAndToken(username, token);
window.location.href = "list.html";
}).catch(function(error) {
// TODO: Show better error messages to user
console.log("Registration failed.");
console.log(error);
setTextHint(error);
})
}
loadExistingSession();
</script>
</body>
</html>

42
Public/reset.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'/>
<title>Schafkopf</title>
<meta name='viewport' content='width=device-width, initial-scale=1'/>
<link rel='stylesheet' type='text/css' href='style.css'/>
<link rel='stylesheet' type='text/css' href='login.css'/>
<script src='api.js'></script>
<script src='login.js'></script>
</head>
<body>
<div id="login-window">
<div id="login-window-vertical-center">
<div id="login-window-inner">
<div id="sheephead-logo">\_______/<br>\ - - /<br>\ | /<br>\_/<br><br>Sheephead</div>
<label for="usrname">Username</span><a href="login.html">Log in instead</a></label>
<input type="text" id="user-name" name="usrname" required/>
<button class="login-buttons standard-button" onclick="resetPassword()">Send recovery email</button>
<div id="user-hint"></div>
</div>
</div>
</div>
<script>
function resetPassword() {
const name = getLoginName()
if (name == "") {
setTextHint("Please enter a name")
return
}
performRecoveryEmailRequest(name)
.then(function(value) {
setTextHint("The email has been sent")
})
.catch(setTextHint)
}
</script>
</body>
</html>

View File

@ -7,6 +7,7 @@
<link rel='stylesheet' type='text/css' media='screen' href='style.css'> <link rel='stylesheet' type='text/css' media='screen' href='style.css'>
<script src='elements.js?v=1'></script> <script src='elements.js?v=1'></script>
<script src='api.js'></script> <script src='api.js'></script>
<script src='storage.js'></script>
<script src='game.js'></script> <script src='game.js'></script>
</head> </head>
<body> <body>
@ -72,25 +73,8 @@
</div> </div>
</div> </div>
</div> </div>
<div id="login-window">
<div id="login-window-vertical-center">
<div id="login-window-inner">
<div id="sheephead-logo">\_______/<br> \ - - / <br> \ | / <br> \_/ <br><br>Sheephead</div>
<label for="usrname">Username</label>
<input type="text" id="user-name" name="usrname" required>
<label for="psw">Password</label>
<input type="password" id="user-pwd" name="psw" required>
<button class="login-buttons standard-button" onclick="registerUser()">Register</button>
<button class="login-buttons standard-button" onclick="loginUser()">Log in</button>
<div id="login-error"></div>
</div>
</div>
</div>
<script> <script>
loadExistingSession() loadExistingSession()
</script> </script>
</body> </body>
</html> </html>

38
Public/storage.js Normal file
View File

@ -0,0 +1,38 @@
// Local storage element identifiers
const localStorageTokenId = "token";
const localStoragePlayerName = "name";
// Can prevent loading of session token, to allow multiple players per browser
const debugMode = false
/*
* Store the player name and session token in local storage.
* Parameter name: The user name of the player
* Parameter token: The session token for the player session
*/
function storePlayerNameAndToken(name, token) {
localStorage.setItem(localStoragePlayerName, name);
localStorage.setItem(localStorageTokenId, token);
}
/*
* Get the last session token from local storage.
*/
function loadSessionToken() {
if (debugMode) {
return debugSessionToken
}
return localStorage.getItem(localStorageTokenId)
}
function storeSessionToken(token) {
if (debugMode) {
debugSessionToken = token
return
}
localStorage.setItem(localStorageTokenId, token)
}
function deleteSessionToken() {
localStorage.removeItem(localStorageTokenId)
}

View File

@ -37,12 +37,6 @@ input {
background-color: var(--standard-background); background-color: var(--standard-background);
} }
#login-window input {
width: 100%;
margin-top: 6px;
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); color: var(--text-color);
@ -60,49 +54,6 @@ body {
position: absolute; position: absolute;
} }
#login-window {
background-color: var(--standard-background);
height: 100%;
width: 100%;
top: 0;
left: 0;
display: table;
position: absolute;
}
#login-window-vertical-center {
display: table-cell;
vertical-align: middle;
}
#login-window-inner {
background-color: var(--element-background);
margin-left: auto;
margin-right: auto;
width: 300px;
border-radius: 10px;
border-style: solid;
border-width: thin;
border-color: var(--element-border);
padding: 10px;
}
#sheephead-logo {
color: rgb(0, 255, 0);
font-family: monospace, monospace;
white-space: pre;
text-align: center;
}
.login-buttons {
width: 100%;
margin-top: 5px;
}
#login-error {
margin-top: 8px;
}
#top-bar { #top-bar {
height: 50px; height: 50px;
background-color: var(--element-background); background-color: var(--element-background);
@ -580,4 +531,4 @@ body {
#player-card8 { #player-card8 {
grid-column: 8; grid-column: 8;
z-index: 8; z-index: 8;
} }