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 - `417`: 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) // 417 return .ok } } /** Use a token from a password reset email to change the password. Headers: - `token`: The one-time recovery token - `password`: The new password for the user Possible responses: - `200`: Success, password changed - `400`: Missing token or password header - `417`: Player name not found or no email registered - `424`: Password could not be hashed */ func resetPlayerPasswordWithEmailToken(_ app: Application) { app.post("player", "password", "new") { req async throws -> HTTPResponseStatus in let token = try req.header(.token) // 400 let hash = try req.hashedPassword() // errors: 400, 424 try await server.updatePassword(password: hash, forResetToken: token, in: req.db) // 417 return .ok } } /** Delete a player. **Headers** - `name`: The name of the player - `password`: The password of the player **Possible errors** - `400`: Missing name or password - `401`: The password or user name is invalid - `424`: The password could not be hashed */ func deletePlayer(_ app: Application) { app.post("player", "delete") { request async throws -> HTTPResponseStatus in let name = try request.header(.name) // 400 let password = try request.header(.password) // 400 let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db) guard try request.password.verify(password, created: hash) else { return .unauthorized // 401 } try await server.deletePlayer(named: name, in: request.db) return .ok } } /** Log in as an existing player. **Headers** - `name`: The name of the player - `password`: The password of the player **Possible errors** - `400`: Missing name or password - `401`: The password or user name is invalid - `424`: The password could not be hashed **Response** - `body`: The session token for the user */ func loginPlayer(_ app: Application) { app.post("player", "login") { request async throws -> String in let name = try request.header(.name) // 400 let password = try request.header(.password) // 400 let hash = try await server.passwordHashForExistingPlayer(named: name, in: request.db) guard try request.password.verify(password, created: hash) else { throw Abort(.unauthorized) // 401 } return server.startNewSessionForRegisteredPlayer(named: name) } } /** Log in using a session token. **Headers** - `token`: The session token of the player **Possible errors** - `400`: Missing token - `401`: The token is invalid **Response** - `body`: The player name associated with the session token */ func resumeSession(_ app: Application) { app.post("player", "resume") { request -> String in let token = try request.header(.token) guard let player = server.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } return player } } /** Log out. **Headers** - `token`: The session token of the player **Possible errors** - `400`: Missing token - Note: The request always succeeds when correctly formed, even for invalid and expired tokens */ func logoutPlayer(_ app: Application) { app.post("player", "logout") { request -> HTTPResponseStatus in let token = try request.header(.token) server.endSession(forSessionToken: token) return .ok } } /** Get the current table of the player, if one exists. **Headers** - `token`: The session token of the player **Possible errors** - `400`: Missing token - `401`: The token is invalid **Response** - `body`: The table info, or an empty string */ func getTableForPlayer(_ app: Application) { app.post("player", "table") { request -> String in let token = try request.header(.token) 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 - 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. **Headers** - `name`: The name of the table - `token`: The session token of the player - `visibility`: The visibility of the table (`private` or `public`) **Errors** - `400`: Missing token, table name or invalid visibility - `401`: The session token is invalid **Response** - `body`: The table id */ func createTable(_ app: Application) { app.post("table", "create") { request -> String in let tableName = try request.header(.name) let token = try request.header(.token) let visibility = try request.header(.visibility) 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. **Headers** - `token`: The session token of the player **Possible errors** - `400`: Missing token - `401`: The session token is invalid **Response** - `body`: A JSON object with a list of public tables (id, name, player list) */ func getPublicTables(_ app: Application) { app.post("tables", "public") { request -> String in let token = try request.header(.token) guard server.isValid(sessionToken: token) else { throw Abort(.unauthorized) // 401 } let list = server.getPublicTableInfos() return try encodeJSON(list) } } /** Join a table. **Headers** - `name`: The table id - `token`: The session token of the player **Possible errors** - `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 */ func joinTable(_ app: Application) { app.post("table", "join") { request -> String in let string = try request.header(.name) let token = try request.header(.token) guard let table = UUID(uuidString: 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. **Headers** - `token`: The session token of the player **Possible errors** - `400`: Missing token - `401`: The session token is invalid */ func leaveTable(_ app: Application) { app.post("table", "leave") { request -> HTTPResponseStatus in let token = try request.header(.token) try await server.leaveTable(playerToken: token, in: request.db) return .ok } } /** Perform an action, either a game selection or a player action. **Headers** - `token`: The session token of the player - `action`: The action to perform **Possible errors** - `400`: Missing token or action - `401`: The session token is invalid - `412`: The action is not allowed */ func performActionForPlayer(_ app: Application) { app.post("player", "action") { request -> HTTPResponseStatus in let token = try request.header(.token) let actionString = try request.header(.action) 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 .ok case .invalidToken: return .unauthorized // 401 case .noTableJoined: return .preconditionFailed // 412 case .tableNotFull: return .preconditionFailed // 412 case .tableStateInvalid: return .preconditionFailed // 412 case .invalidCard: return .preconditionFailed // 412 } } } /** Play a card as the active player. **Headers** - `token`: The session token of the player - `action`: The id of the card to play **Possible errors** - `400`: Missing token or card id - `401`: The session token is invalid - `412`: The action is not allowed */ func playCard(_ app: Application) { app.post("player", "card") { request async throws -> HTTPResponseStatus in let token = try request.header(.token) let cardId = try request.header(.action) guard let card = Card(id: cardId) else { throw Abort(.badRequest) } switch try await server.play(card: card, playerToken: token, in: request.db) { case .success: return .ok case .invalidToken: return .unauthorized // 401 case .noTableJoined: return .preconditionFailed // 412 case .tableStateInvalid: return .preconditionFailed // 412 case .invalidCard: return .preconditionFailed // 412 case .tableNotFull: return .preconditionFailed // 412 } } }