diff --git a/.gitignore b/.gitignore
index 5b7445c..68d8b65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ DerivedData/
db.sqlite
.swiftpm
+Package.resolved
diff --git a/Public/game.js b/Public/game.js
new file mode 100644
index 0000000..b248d59
--- /dev/null
+++ b/Public/game.js
@@ -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() {
+
+}
diff --git a/Public/schafkopf.html b/Public/schafkopf.html
new file mode 100644
index 0000000..905e2df
--- /dev/null
+++ b/Public/schafkopf.html
@@ -0,0 +1,30 @@
+
+
+
+
+ Schafkopf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Public/style.css b/Public/style.css
new file mode 100644
index 0000000..d714f76
--- /dev/null
+++ b/Public/style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/Sources/App/Model/Database.swift b/Sources/App/Model/Database.swift
new file mode 100644
index 0000000..6edf5c3
--- /dev/null
+++ b/Sources/App/Model/Database.swift
@@ -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()
+
+ /// 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()
+ }
+ }
+}
diff --git a/Sources/App/Model/Table.swift b/Sources/App/Model/Table.swift
new file mode 100644
index 0000000..6652d08
--- /dev/null
+++ b/Sources/App/Model/Table.swift
@@ -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
+ }
+}
diff --git a/Sources/App/Model/User.swift b/Sources/App/Model/User.swift
new file mode 100644
index 0000000..fb6c44d
--- /dev/null
+++ b/Sources/App/Model/User.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+
+struct User: Codable {
+
+ let name: String
+
+ let passwordHash: Data
+}
diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift
index 800d36c..78f9455 100644
--- a/Sources/App/configure.swift
+++ b/Sources/App/configure.swift
@@ -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)
}
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index 6bb9c5c..d318280 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -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 ""
}
}