First registration/login interface

This commit is contained in:
Christoph Hagen 2021-11-27 11:59:13 +01:00
parent 11dde8b8ab
commit b87dce55a8
9 changed files with 408 additions and 4 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ DerivedData/
db.sqlite
.swiftpm
Package.resolved

52
Public/game.js Normal file
View File

@ -0,0 +1,52 @@
function hideLoginWindow() {
document.getElementById("signup-window").style.display = "none"
}
async function registerUser() {
let username = document.getElementById("user-name").value
let password = document.getElementById("user-pwd").value
errorField = document.getElementById("login-error")
console.log("Registration started");
fetch("/create/user/" + username + "/" + password, { method: 'POST' })
.then(function(response) {
if (response.status == 200) { // Success
return response.text()
}
if (response.status == 400) { // Bad request
throw Error("The request had an error")
}
if (response.status == 409) { // Conflict
throw Error("A user with the same name is already registered")
}
throw Error("Unexpected response: " + response.statusText)
}).then(function(text) {
localStorage.setItem('token', text)
hideLoginWindow()
console.log("Registered")
}).catch(function(error) {
errorField.innerHTML = error.message
console.log(error)
return
})
}
function loadExistingSession() {
console.log("Checking to resume session");
const token = localStorage.getItem('token');
if (token) {
console.log("Resuming session with token " + token);
resumeSession(token);
}
}
function resumeSession(token) {
localStorage.removeItem('token');
hideLoginWindow()
}
function loginUser() {
}

30
Public/schafkopf.html Normal file
View File

@ -0,0 +1,30 @@
<!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'>
</head>
<body>
<div class="signup-backdrop" id="signup-window">
<div class="signup-window-vertical">
<div class="signup-window">
<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" onclick="registerUser()">Register</button>
<button class="login-buttons" onclick="loginUser()">Log in</button>
<div id="login-error"></div>
</div>
</div>
</div>
<script src='game.js'></script>
<script>
loadExistingSession()
</script>
</body>
</html>

61
Public/style.css Normal file
View File

@ -0,0 +1,61 @@
/* Style all input fields */
input {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
margin-top: 6px;
margin-bottom: 16px;
}
html * {
font-family:-apple-system, BlinkMacSystemFont, "SF Hello", "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif;
}
body, html {
min-height: 100%;
height: 100%;
}
.signup-backdrop {
background-color: #fff;
height: 100%;
width: 100%;
top: 0;
left: 0;
display: table;
position: absolute;
}
.signup-window-vertical {
display: table-cell;
vertical-align: middle;
}
.signup-window {
background-color: #f1f1f1;
margin-left: auto;
margin-right: auto;
width: 300px;
border-radius: 10px;
border-style: solid;
border-width: thin;
border-color: darkgray;
padding: 10px;
}
.login-buttons {
width: 100%;
padding: 10px;
background-color: #ccc;
color: #000;
border-style: hidden;
border-radius: 5px;
margin-top: 5px;
font-size: medium;
}
.login-buttons:hover {
background-color: #ddd;
}

View File

@ -0,0 +1,141 @@
import Foundation
import Crypto
let playerPerTable = 4
final class Database {
/// A mapping between usernames and their password hashes
private var userPasswordHashes = [String: String]()
/// A mapping between usernames and generated access tokens for a session
private var authTokenForUser = [String: String]()
/// A reverse mapping between generated access tokens and usernames
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() {
}
/**
Check if a user exists.
- Parameter name: The name of the user
- Returns: true, if the user exists
*/
func has(user: String) -> Bool {
userPasswordHashes[user] != nil
}
/**
Get the password hash for a user, if the user exists.
- Parameter name: The name of the user
- Returns: The stored password hash, if the user exists
*/
func hash(ofUser name: String) -> String? {
userPasswordHashes[name]
}
/**
Create a new user and assign an access token.
- Parameter name: The name of the new user
- Parameter hash: The password hash of the user
- Returns: The generated access token for the session
*/
func add(user name: String, hash: String) -> String {
self.userPasswordHashes[name] = hash
return startSession(forUser: name)
}
/**
Start a new session for an existing user.
- Parameter name: The user name
- Returns: The generated access token for the session
*/
func startSession(forUser name: String) -> String {
let token = newToken()
self.authTokenForUser[name] = token
self.userForToken[token] = name
return token
}
/**
Get the user for a session 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 {
tableNames.contains { $0.value == name }
}
func tableExists(withId id: String) -> Bool {
tableNames[id] != nil
}
func tableIsFull(withId id: String) -> Bool {
tablePlayers[id]!.count < 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: String, player: String, visible: Bool) -> String {
let tableId = newToken()
tableNames[tableId] = tableId
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()
}
func join(tableId: String, player: String) {
tablePlayers[tableId]!.append(player)
if let oldTable = playerTables[tableId] {
remove(player: player, fromTable: oldTable)
}
playerTables[tableId] = tableId
}
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()
}
}
}

View File

@ -0,0 +1,17 @@
import Foundation
struct TableInfo: Codable {
let id: String
let name: String
var players: [String]
}
extension TableInfo: Comparable {
static func < (lhs: TableInfo, rhs: TableInfo) -> Bool {
lhs.name < rhs.name
}
}

View File

@ -0,0 +1,9 @@
import Foundation
struct User: Codable {
let name: String
let passwordHash: Data
}

View File

@ -1,10 +1,13 @@
import Vapor
var database: Database!
// configures your application
public func configure(_ app: Application) throws {
// uncomment to serve files from /Public folder
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
// serve files from /Public folder
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
database = Database()
// register routes
try routes(app)
}

View File

@ -1,11 +1,101 @@
import Vapor
private let encoder = JSONEncoder()
func routes(_ app: Application) throws {
app.get { req in
return "It works!"
}
app.get("hello") { req -> String in
return "Hello, world!"
app.post("create", "user", ":name", ":hash") { req -> String in
guard let name = req.parameters.get("name"),
let hash = req.parameters.get("hash") else {
throw Abort(.badRequest)
}
let digest = try req.password.hash(hash)
guard !database.has(user: name) else {
throw Abort(.conflict)
}
let token = database.add(user: name, hash: digest)
return token
}
app.get("create", "session", ":name", ":hash") { req -> String in
guard let name = req.parameters.get("name"),
let hash = req.parameters.get("hash") else {
throw Abort(.badRequest)
}
guard let digest = database.hash(ofUser: name),
try req.password.verify(hash, created: digest) else {
throw Abort(.forbidden)
}
let token = database.startSession(forUser: name)
return token
}
app.get("session", "resume", ":token") { req -> String in
guard let token = req.parameters.get("token") else {
throw Abort(.badRequest)
}
guard let user = database.user(forToken: token) else {
throw Abort(.forbidden)
}
return user
}
// TODO: Improve token handling (it will be logged when included in url!)
app.get("create", "table", ":visibility", ":name", ":token") { req -> String in
guard let name = req.parameters.get("name"),
let token = req.parameters.get("token"),
let visibility = req.parameters.get("visibility") else {
throw Abort(.badRequest)
}
let isVisible: Bool
if visibility == "private" {
isVisible = false
} else if visibility == "public" {
isVisible = true
} else {
throw Abort(.badRequest)
}
guard let user = database.user(forToken: token) else {
throw Abort(.forbidden)
}
guard !database.tableExists(named: name) else {
throw Abort(.conflict)
}
let tableId = database.createTable(named: name, player: user, visible: isVisible)
return tableId
}
app.get("tables", "public", ":token") { req -> String in
guard let token = req.parameters.get("token") else {
throw Abort(.badRequest)
}
guard let _ = database.user(forToken: token) else {
throw Abort(.forbidden)
}
let list = database.getPublicTableInfos()
return try encoder.encode(list).base64EncodedString()
}
app.post("table", "join", ":table", ":token") { req -> String in
guard let table = req.parameters.get("table"),
let token = req.parameters.get("token") else {
throw Abort(.badRequest)
}
guard let player = database.user(forToken: token) else {
throw Abort(.forbidden)
}
guard database.tableExists(withId: table) else {
throw Abort(.notFound)
}
guard !database.tableIsFull(withId: table) else {
throw Abort(.notAcceptable)
}
database.join(tableId: table, player: player)
return ""
}
}