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 encodeJSON(_ response: T) throws -> String where T: Encodable { let data = try encoder.encode(response) return String(data: data, encoding: .utf8)! } 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 -> EventLoopFuture 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 } // Can throw conflict (409) return server.registerPlayer(named: name, hash: hash, in: req.db) } /** 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 -> EventLoopFuture in guard let name = req.parameters.get("name"), let password = req.body.string else { throw Abort(.badRequest) // 400 } return server.passwordHashForExistingPlayer(named: name, in: req.db) .guard({ hash in (try? req.password.verify(password, created: hash)) ?? false }, else: Abort(.forbidden)).flatMap { _ in server.deletePlayer(named: name, in: req.db) }.map { "" } } /** 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 -> EventLoopFuture in guard let name = req.parameters.get("name"), let password = req.body.string else { throw Abort(.badRequest) // 400 } return server.passwordHashForExistingPlayer(named: name, in: req.db) .guard({ hash in (try? req.password.verify(password, created: hash)) ?? false }, else: Abort(.forbidden)).map { _ in server.startNewSessionForRegisteredPlayer(named: name) } } /** 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 = server.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 } server.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 info, 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 = server.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } guard let info = server.currentTableOfPlayer(named: player) else { return "" } return try encodeJSON(info) } /** 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 server.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 -> EventLoopFuture 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 isPublic: Bool if visibility == "private" { isPublic = false } else if visibility == "public" { isPublic = true } else { throw Abort(.badRequest) // 400 } guard let player = server.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } return server.createTable(named: tableName, player: player, isPublic: isPublic, in: req.db) .flatMapThrowing(encodeJSON) } /** 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 server.isValid(sessionToken: token) else { throw Abort(.forbidden) // 403 } let list = server.getPublicTableInfos() return try encodeJSON(list) } /** 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 - 403: The player already sits at another table - 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 -> EventLoopFuture in guard let string = req.parameters.get("table"), let table = UUID(uuidString: string), let token = req.body.string else { throw Abort(.badRequest) } return server.join(tableId: table, playerToken: token, in: req.db) .flatMapThrowing(encodeJSON) } /** 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 -> EventLoopFuture in guard let token = req.body.string else { throw Abort(.badRequest) } return server.leaveTable(playerToken: token, in: req.db).map { "" } } app.post("player", "action", ":action") { req -> String in guard let token = req.body.string, let actionString = req.parameters.get("action") else { throw Abort(.badRequest) } let result: PlayerActionResult if let action = PlayerAction(rawValue: actionString) { result = server.performAction(playerToken: token, action: action) } else if let game = GameType(rawValue: actionString) { result = server.select(game: game, playerToken: token) } else { throw Abort(.badRequest) } switch result { case .success: return "" case .invalidToken: throw Abort(.unauthorized) // 401 case .noTableJoined: throw Abort(.preconditionFailed) // 412 case .tableNotFull: throw Abort(.preconditionFailed) // 412 case .tableStateInvalid: throw Abort(.preconditionFailed) // 412 case .invalidCard: throw Abort(.preconditionFailed) // 412 } } app.post("player", "card", ":card") { req -> String in guard let token = req.body.string, let cardId = req.parameters.get("card"), let card = Card(id: cardId) else { throw Abort(.badRequest) } switch server.play(card: card, playerToken: token) { case .success: return "" case .invalidToken: throw Abort(.unauthorized) // 401 case .noTableJoined: throw Abort(.preconditionFailed) // 412 case .tableStateInvalid: throw Abort(.preconditionFailed) // 412 case .invalidCard: throw Abort(.preconditionFailed) // 412 case .tableNotFull: throw Abort(.preconditionFailed) // 412 } } }