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 { // 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 password = req.body.string else { throw Abort(.badRequest) // 400 } guard name.count < maximumPlayerNameLength, password.count < maximumPasswordLength else { throw Abort(.notAcceptable) // 406 } 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 } return token } /** 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 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 } 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 } /** 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 player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } return player } /** 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 "" } /** Get the current table of the player, if one exists. - Parameter token: The session token of the player, as a string in the request body - Throws: - 400: Missing token - 401: Invalid token - Returns: The table id, or an empty string */ app.post("player", "table") { req -> String in guard let token = req.body.string else { throw Abort(.badRequest) // 400 } guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } return database.currentTableOfPlayer(named: player) } /** Start a new websocket connection for the client to receive table updates from the server - Returns: Nothing - Note: The first (and only) message from the client over the connection must be a valid session token. */ app.webSocket("session", "start") { req, socket in socket.onText { socket, text in guard database.startSession(socket: socket, sessionToken: text) else { _ = socket.close() return } } } // 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) // 400 } guard let player = database.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } let tableId = database.createTable(named: tableName, player: player, visible: isVisible) return tableId } /** 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 database.isValid(sessionToken: token) else { throw Abort(.forbidden) // 403 } let list = database.getPublicTableInfos() let data = try encoder.encode(list) return String(data: data, encoding: .utf8)! } /** 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 - 410: The table id doesn't exist - 417: 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.body.string else { throw Abort(.badRequest) } switch database.join(tableId: table, playerToken: token) { case .invalidToken: throw Abort(.unauthorized) // 401 case .tableNotFound: throw Abort(.gone) // 410 case .tableIsFull: throw Abort(.expectationFailed) // 417 case .success: return "" } } /** Leave the current table. - 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 - Returns: Nothing */ app.post("table", "leave") { req -> String in guard let token = req.body.string else { throw Abort(.badRequest) } guard database.leaveTable(playerToken: token) else { throw Abort(.unauthorized) // 401 } return "" } }