+
diff --git a/Public/style.css b/Public/style.css
index d714f76..64c3562 100644
--- a/Public/style.css
+++ b/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 */
+
+.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 {
- width: 100%;
padding: 8px;
- border: 1px solid #ccc;
+ border: 1px solid var(--element-border);
border-radius: 4px;
box-sizing: border-box;
+ background-color: var(--standard-background);
+}
+
+.signup-window input {
+ width: 100%;
margin-top: 6px;
margin-bottom: 16px;
}
html * {
font-family:-apple-system, BlinkMacSystemFont, "SF Hello", "Helvetica Neue", Helvetica, Arial, Verdana, sans-serif;
+ color: var(--text-color);
}
body, html {
min-height: 100%;
height: 100%;
+ margin: 0px;
+ background-color: var(--standard-background);
}
-.signup-backdrop {
- background-color: #fff;
+.signup-window {
+ background-color: var(--standard-background);
height: 100%;
width: 100%;
top: 0;
@@ -28,34 +60,142 @@ body, html {
position: absolute;
}
-.signup-window-vertical {
+.signup-window-vertical-center {
display: table-cell;
vertical-align: middle;
}
-.signup-window {
- background-color: #f1f1f1;
+.signup-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: darkgray;
+ border-color: var(--element-border);
padding: 10px;
}
.login-buttons {
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;
- background-color: #ccc;
- color: #000;
+ margin-top: 10px;
+ background-color: var(--element-background);
border-style: hidden;
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;
}
-.login-buttons:hover {
- background-color: #ddd;
-}
\ No newline at end of file
+.table-subtitle {
+ grid-column: 2;
+ grid-row: 2;
+ font-size: small;
+}
diff --git a/Sources/App/Model/Crypto+Extensions.swift b/Sources/App/Model/Crypto+Extensions.swift
new file mode 100644
index 0000000..6d40f58
--- /dev/null
+++ b/Sources/App/Model/Crypto+Extensions.swift
@@ -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()
+ }
+ }
+}
diff --git a/Sources/App/Model/Database.swift b/Sources/App/Model/Database.swift
index 6edf5c3..20b2204 100644
--- a/Sources/App/Model/Database.swift
+++ b/Sources/App/Model/Database.swift
@@ -1,62 +1,68 @@
import Foundation
-import Crypto
+import Vapor
let playerPerTable = 4
+typealias TableId = String
+typealias TableName = String
+
final class Database {
- /// A mapping between usernames and their password hashes
- private var userPasswordHashes = [String: String]()
+ private let players: PlayerManagement
- /// A mapping between usernames and generated access tokens for a session
- private var authTokenForUser = [String: String]()
+ private let tables: TableManagement
- /// 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]()
+ private var sessions: [SessionToken : WebSocket]
init() {
-
+ self.players = PlayerManagement()
+ self.tables = TableManagement()
+ self.sessions = [:]
+ // TODO: Load server data from disk
+ // TODO: Save data to disk
}
- /**
- 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
+ // MARK: Players & Sessions
+
+ func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
+ players.registerPlayer(named: name, hash: hash)
}
- /**
- 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]
+ func passwordHashForExistingPlayer(named name: PlayerName) -> PasswordHash? {
+ players.passwordHash(ofRegisteredPlayer: 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)
+ func deletePlayer(named name: PlayerName) {
+ if let sessionToken = players.deletePlayer(named: name) {
+ closeAndRemoveSession(for: sessionToken)
+ }
+ // TODO: Delete player from tables
+ }
+
+ func isValid(sessionToken token: SessionToken) -> Bool {
+ 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
- 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
+ func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
+ players.startNewSessionForRegisteredPlayer(named: name)
}
- /**
- 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 registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
+ players.registeredPlayerExists(withSessionToken: token)
}
- func tableExists(named name: String) -> Bool {
- tableNames.contains { $0.value == name }
+ // MARK: Tables
+
+ func tableExists(withId id: TableId) -> Bool {
+ tables.tableExists(withId: id)
}
- func tableExists(withId id: String) -> Bool {
- tableNames[id] != nil
- }
-
- func tableIsFull(withId id: String) -> Bool {
- tablePlayers[id]!.count < playerPerTable
+ func tableIsFull(withId id: TableId) -> Bool {
+ tables.tableIsFull(withId: id)
}
/**
@@ -99,43 +95,22 @@ final class Database {
- 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 createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
+ tables.createTable(named: name, player: player, visible: visible)
}
func getPublicTableInfos() -> [TableInfo] {
- publicTables.map { tableId in
- TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!)
- }.sorted()
+ tables.getPublicTableInfos()
}
- 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()
- }
+ func join(tableId: TableId, player: PlayerName) {
+ let playersAtTable = tables.join(tableId: tableId, player: player)
+ playersAtTable
+ .compactMap { players.sessionToken(forPlayer: $0) } // Session Tokens
+ .compactMap { sessions[$0] } // Sockets
+ .forEach { socket in
+ // TODO: Notify sessions about changed players
+ // socket.send("")
+ }
}
}
diff --git a/Sources/App/Model/PlayerManagement.swift b/Sources/App/Model/PlayerManagement.swift
new file mode 100644
index 0000000..75f4e4a
--- /dev/null
+++ b/Sources/App/Model/PlayerManagement.swift
@@ -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]
+ }
+
+
+}
diff --git a/Sources/App/Model/TableManagement.swift b/Sources/App/Model/TableManagement.swift
new file mode 100644
index 0000000..b35184e
--- /dev/null
+++ b/Sources/App/Model/TableManagement.swift
@@ -0,0 +1,75 @@
+import Foundation
+
+final class TableManagement {
+
+ /// 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 = [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 }
+ }
+}
diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift
index d318280..8ccee40 100644
--- a/Sources/App/routes.swift
+++ b/Sources/App/routes.swift
@@ -1,99 +1,230 @@
import Vapor
+/// The JSON encoder for responses
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 {
- 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"),
- let hash = req.parameters.get("hash") else {
- throw Abort(.badRequest)
- }
- let digest = try req.password.hash(hash)
+ let password = req.body.string else {
+ throw Abort(.badRequest) // 400
+ }
+ guard name.count < maximumPlayerNameLength,
+ password.count < maximumPasswordLength else {
+ throw Abort(.notAcceptable) // 406
+ }
- guard !database.has(user: name) else {
- throw Abort(.conflict)
+ guard let hash = try? req.password.hash(password) else {
+ 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
}
- 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"),
- let hash = req.parameters.get("hash") else {
- throw Abort(.badRequest)
+ let password = req.body.string else {
+ throw Abort(.badRequest) // 400
+ }
+ guard let hash = database.passwordHashForExistingPlayer(named: name) else {
+ throw Abort(.forbidden) // 403
}
- guard let digest = database.hash(ofUser: name),
- try req.password.verify(hash, created: digest) else {
- throw Abort(.forbidden)
+ guard let isValid = try? req.password.verify(password, created: hash) else {
+ throw Abort(.failedDependency) // 424
}
- 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
}
- app.get("session", "resume", ":token") { req -> String in
- guard let token = req.parameters.get("token") else {
- throw Abort(.badRequest)
+ /**
+ Log in using a session token.
+ - 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 {
- throw Abort(.forbidden)
+ guard let player = database.registeredPlayerExists(withSessionToken: token) else {
+ 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
- guard let name = req.parameters.get("name"),
- let token = req.parameters.get("token"),
- let visibility = req.parameters.get("visibility") else {
- throw Abort(.badRequest)
+ /**
+ Log out.
+ - Parameter name: The name of the player, included in the url
+ - Parameter token: The session token of the player, as a string in the request body
+ - Throws:
+ - 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
if visibility == "private" {
isVisible = false
} else if visibility == "public" {
isVisible = true
} else {
- throw Abort(.badRequest)
+ throw Abort(.badRequest) // 400
}
- guard let user = database.user(forToken: token) else {
- throw Abort(.forbidden)
+ guard let player = database.registeredPlayerExists(withSessionToken: token) else {
+ throw Abort(.unauthorized) // 401
}
- guard !database.tableExists(named: name) else {
- throw Abort(.conflict)
- }
- let tableId = database.createTable(named: name, player: user, visible: isVisible)
+ let tableId = database.createTable(named: tableName, player: player, visible: isVisible)
return tableId
}
- app.get("tables", "public", ":token") { req -> String in
- guard let token = req.parameters.get("token") else {
- throw Abort(.badRequest)
+ /**
+ List the public tables.
+ - 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 {
- throw Abort(.forbidden)
+ guard database.isValid(sessionToken: token) else {
+ throw Abort(.forbidden) // 403
}
let list = database.getPublicTableInfos()
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"),
- let token = req.parameters.get("token") else {
- throw Abort(.badRequest)
- }
- guard let player = database.user(forToken: token) else {
- throw Abort(.forbidden)
+ let token = req.body.string else {
+ throw Abort(.badRequest)
+ }
+ guard let player = database.registeredPlayerExists(withSessionToken: token) else {
+ throw Abort(.unauthorized) // 401
}
guard database.tableExists(withId: table) else {
- throw Abort(.notFound)
+ throw Abort(.notFound) // 404
}
guard !database.tableIsFull(withId: table) else {
- throw Abort(.notAcceptable)
+ throw Abort(.notAcceptable) // 406
}
database.join(tableId: table, player: player)
return ""