First registration/login interface
This commit is contained in:
parent
11dde8b8ab
commit
b87dce55a8
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ DerivedData/
|
|||||||
db.sqlite
|
db.sqlite
|
||||||
.swiftpm
|
.swiftpm
|
||||||
|
|
||||||
|
Package.resolved
|
||||||
|
52
Public/game.js
Normal file
52
Public/game.js
Normal 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
30
Public/schafkopf.html
Normal 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
61
Public/style.css
Normal 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;
|
||||||
|
}
|
141
Sources/App/Model/Database.swift
Normal file
141
Sources/App/Model/Database.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
Sources/App/Model/Table.swift
Normal file
17
Sources/App/Model/Table.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
9
Sources/App/Model/User.swift
Normal file
9
Sources/App/Model/User.swift
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
struct User: Codable {
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let passwordHash: Data
|
||||||
|
}
|
@ -1,10 +1,13 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
|
var database: Database!
|
||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
// uncomment to serve files from /Public folder
|
// serve files from /Public folder
|
||||||
// app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory))
|
||||||
|
|
||||||
|
database = Database()
|
||||||
// register routes
|
// register routes
|
||||||
try routes(app)
|
try routes(app)
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,101 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
func routes(_ app: Application) throws {
|
||||||
app.get { req in
|
app.get { req in
|
||||||
return "It works!"
|
return "It works!"
|
||||||
}
|
}
|
||||||
|
|
||||||
app.get("hello") { req -> String in
|
app.post("create", "user", ":name", ":hash") { req -> String in
|
||||||
return "Hello, world!"
|
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 ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user