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 { registerPlayer(app) requestPlayerPasswordReset(app) resetPlayerPasswordWithEmailToken(app) deletePlayer(app) loginPlayer(app) resumeSession(app) logoutPlayer(app) getTableForPlayer(app) openWebsocket(app) createTable(app) getPublicTables(app) joinTable(app) leaveTable(app) performActionForPlayer(app) playCard(app) } // MARK: Players & Sessions /** Create a new player. Headers: - `name`: The username of the player - `password`: The password of the player - `email`: Optional email address for password reset Possible responses: - `200`: On success, with the session token for the registered user in the reponse body - `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 */ func registerPlayer(_ app: Application) { app.post("player", "register") { request async throws -> SessionToken in let name = try request.header(.name) let hash = try request.hashedPassword() // errors: 400, 424 let mail = request.optionalHeader(.email)?.trimmed.nonEmpty guard name.count < maximumPlayerNameLength else { throw Abort(.notAcceptable) // 406 } // Can throw conflict (409) // if either the player exists, or the email is already in use return try await server.registerPlayer(named: name, hash: hash, email: mail, in: request.db) } } /** Request an email to reset the password of a player. Headers: - `name`: The player name Possible responses: - `200`: Success, email will be sent - `400`: Missing name header - `404`: Player name not found or no email registered */ func requestPlayerPasswordReset(_ app: Application) { app.post("player", "password", "reset") { request async throws -> HTTPResponseStatus in let name = try request.header(.name) // Error: 400 try await server.sendPasswordResetEmailIfPossible(name: name, in: request.db) return .ok } } /** 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 */ func deletePlayer(_ app: Application) { app.post("player", "delete", ":name") { request async throws -> HTTPResponseStatus in guard let name = request.parameters.get("name"), let password = request.body.string else { return .badRequest // 400 } let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db) guard try request.password.verify(password, created: hash) else { return .forbidden // 403 } try await server.deletePlayer(named: name, in: request.db) return .ok } } /** 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 */ func loginPlayer(_ app: Application) { app.post("player", "login", ":name") { request async throws -> String in guard let name = request.parameters.get("name"), let password = request.body.string else { throw Abort(.badRequest) // 400 } let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db) guard try request.password.verify(password, created: hash) else { throw Abort(.forbidden) // 403 } return 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 */ func resumeSession(_ app: Application) { 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 */ func logoutPlayer(_ app: Application) { 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 */ func getTableForPlayer(_ app: Application) { 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. */ func openWebsocket(_ app: Application) { 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 */ func createTable(_ app: Application) { app.post("table", "create", ":visibility", ":name") { request -> String in guard let visibility = request.parameters.get("visibility"), let tableName = request.parameters.get("name"), let token = request.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 } let result = try await server.createTable(named: tableName, player: player, isPublic: isPublic, in: request.db) return try encodeJSON(result) } } /** 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) */ func getPublicTables(_ app: Application) { 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 */ func joinTable(_ app: Application) { app.post("table", "join", ":table") { request -> String in guard let string = request.parameters.get("table"), let table = UUID(uuidString: string), let token = request.body.string else { throw Abort(.badRequest) } let result = try await server.join(tableId: table, playerToken: token, in: request.db) return try encodeJSON(result) } } /** 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 */ func leaveTable(_ app: Application) { app.post("table", "leave") { request -> HTTPResponseStatus in guard let token = request.body.string else { throw Abort(.badRequest) } try await server.leaveTable(playerToken: token, in: request.db) return .ok } } func performActionForPlayer(_ app: Application) { 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 } } } func playCard(_ app: Application) { 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 } } }