2021-11-25 19:15:38 +01:00
|
|
|
import Vapor
|
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/// The JSON encoder for responses
|
2021-11-27 11:59:13 +01:00
|
|
|
private let encoder = JSONEncoder()
|
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/// The maximum length of a valid player name
|
|
|
|
private let maximumPlayerNameLength = 40
|
|
|
|
|
|
|
|
/// The maximum length of a valid password
|
|
|
|
private let maximumPasswordLength = 40
|
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
func encodeJSON<T>(_ response: T) throws -> String where T: Encodable {
|
|
|
|
let data = try encoder.encode(response)
|
|
|
|
return String(data: data, encoding: .utf8)!
|
|
|
|
}
|
2021-11-25 19:15:38 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
func routes(_ app: Application) throws {
|
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
// MARK: Players & Sessions
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("player", "register", ":name") { req -> EventLoopFuture<SessionToken> in
|
2021-11-27 11:59:13 +01:00
|
|
|
guard let name = req.parameters.get("name"),
|
2021-11-28 15:53:47 +01:00
|
|
|
let password = req.body.string else {
|
|
|
|
throw Abort(.badRequest) // 400
|
|
|
|
}
|
|
|
|
guard name.count < maximumPlayerNameLength,
|
|
|
|
password.count < maximumPasswordLength else {
|
|
|
|
throw Abort(.notAcceptable) // 406
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
guard let hash = try? req.password.hash(password) else {
|
|
|
|
throw Abort(.failedDependency) // 424
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
// Can throw conflict (409)
|
|
|
|
return server.registerPlayer(named: name, hash: hash, in: req.db)
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("player", "delete", ":name") { req -> EventLoopFuture<String> in
|
2021-11-27 11:59:13 +01:00
|
|
|
guard let name = req.parameters.get("name"),
|
2021-11-28 15:53:47 +01:00
|
|
|
let password = req.body.string else {
|
|
|
|
throw Abort(.badRequest) // 400
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
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 { "" }
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("player", "login", ":name") { req -> EventLoopFuture<String> in
|
2021-11-28 15:53:47 +01:00
|
|
|
guard let name = req.parameters.get("name"),
|
|
|
|
let password = req.body.string else {
|
|
|
|
throw Abort(.badRequest) // 400
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
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)
|
|
|
|
}
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
2021-11-28 15:53:47 +01:00
|
|
|
throw Abort(.unauthorized) // 401
|
|
|
|
}
|
|
|
|
return player
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
server.endSession(forSessionToken: token)
|
2021-11-28 15:53:47 +01:00
|
|
|
return ""
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
2021-11-29 11:54:50 +01:00
|
|
|
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
|
2021-12-03 18:03:29 +01:00
|
|
|
- Returns: The table info, or an empty string
|
2021-11-29 11:54:50 +01:00
|
|
|
*/
|
|
|
|
app.post("player", "table") { req -> String in
|
|
|
|
guard let token = req.body.string else {
|
|
|
|
throw Abort(.badRequest) // 400
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
2021-11-29 11:54:50 +01:00
|
|
|
throw Abort(.unauthorized) // 401
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
guard let info = server.currentTableOfPlayer(named: player) else {
|
2021-12-03 18:03:29 +01:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return try encodeJSON(info)
|
2021-11-29 11:54:50 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-29 11:54:50 +01:00
|
|
|
/**
|
|
|
|
Start a new websocket connection for the client to receive table updates from the server
|
2021-11-28 15:53:47 +01:00
|
|
|
- Returns: Nothing
|
2021-11-29 11:54:50 +01:00
|
|
|
- Note: The first (and only) message from the client over the connection must be a valid session token.
|
2021-11-28 15:53:47 +01:00
|
|
|
*/
|
|
|
|
app.webSocket("session", "start") { req, socket in
|
|
|
|
socket.onText { socket, text in
|
2021-12-22 22:13:09 +01:00
|
|
|
guard server.startSession(socket: socket, sessionToken: text) else {
|
2021-11-28 15:53:47 +01:00
|
|
|
_ = socket.close()
|
|
|
|
return
|
|
|
|
}
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-11-28 15:53:47 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
// MARK: Tables
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("table", "create", ":visibility", ":name") { req -> EventLoopFuture<String> in
|
2021-11-28 15:53:47 +01:00
|
|
|
guard let visibility = req.parameters.get("visibility"),
|
|
|
|
let tableName = req.parameters.get("name"),
|
|
|
|
let token = req.body.string else {
|
|
|
|
throw Abort(.badRequest) // 400
|
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
let isPublic: Bool
|
2021-11-27 11:59:13 +01:00
|
|
|
if visibility == "private" {
|
2021-12-03 18:03:29 +01:00
|
|
|
isPublic = false
|
2021-11-27 11:59:13 +01:00
|
|
|
} else if visibility == "public" {
|
2021-12-03 18:03:29 +01:00
|
|
|
isPublic = true
|
2021-11-27 11:59:13 +01:00
|
|
|
} else {
|
2021-11-28 15:53:47 +01:00
|
|
|
throw Abort(.badRequest) // 400
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-12-22 22:13:09 +01:00
|
|
|
guard let player = server.registeredPlayerExists(withSessionToken: token) else {
|
2021-11-28 15:53:47 +01:00
|
|
|
throw Abort(.unauthorized) // 401
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
return server.createTable(named: tableName, player: player, isPublic: isPublic, in: req.db)
|
|
|
|
.flatMapThrowing(encodeJSON)
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
guard server.isValid(sessionToken: token) else {
|
2021-11-28 15:53:47 +01:00
|
|
|
throw Abort(.forbidden) // 403
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
let list = server.getPublicTableInfos()
|
2021-12-03 18:03:29 +01:00
|
|
|
return try encodeJSON(list)
|
2021-11-27 11:59:13 +01:00
|
|
|
}
|
2021-12-03 18:03:29 +01:00
|
|
|
|
2021-11-28 15:53:47 +01:00
|
|
|
/**
|
|
|
|
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
|
2021-12-03 18:03:29 +01:00
|
|
|
- 403: The player already sits at another table
|
2021-11-29 11:54:50 +01:00
|
|
|
- 410: The table id doesn't exist
|
|
|
|
- 417: The table is already full and can't be joined
|
2021-11-28 15:53:47 +01:00
|
|
|
- Returns: Nothing
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("table", "join", ":table") { req -> EventLoopFuture<String> in
|
|
|
|
guard let string = req.parameters.get("table"),
|
|
|
|
let table = UUID(uuidString: string),
|
2021-11-28 15:53:47 +01:00
|
|
|
let token = req.body.string else {
|
|
|
|
throw Abort(.badRequest)
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
return server.join(tableId: table, playerToken: token, in: req.db)
|
|
|
|
.flatMapThrowing(encodeJSON)
|
2021-11-25 19:15:38 +01:00
|
|
|
}
|
2021-11-30 11:56:51 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
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
|
|
|
|
*/
|
2021-12-22 22:13:09 +01:00
|
|
|
app.post("table", "leave") { req -> EventLoopFuture<String> in
|
2021-11-30 11:56:51 +01:00
|
|
|
guard let token = req.body.string else {
|
|
|
|
throw Abort(.badRequest)
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
return server.leaveTable(playerToken: token, in: req.db).map { "" }
|
2021-11-30 11:56:51 +01:00
|
|
|
}
|
2021-12-01 22:49:54 +01:00
|
|
|
|
2021-12-03 18:03:29 +01:00
|
|
|
app.post("player", "action", ":action") { req -> String in
|
|
|
|
guard let token = req.body.string,
|
2021-12-06 18:28:35 +01:00
|
|
|
let actionString = req.parameters.get("action") else {
|
2021-12-03 18:03:29 +01:00
|
|
|
throw Abort(.badRequest)
|
|
|
|
}
|
2021-12-06 18:28:35 +01:00
|
|
|
let result: PlayerActionResult
|
2021-12-09 11:10:20 +01:00
|
|
|
if let action = PlayerAction(rawValue: actionString) {
|
2021-12-22 22:13:09 +01:00
|
|
|
result = server.performAction(playerToken: token, action: action)
|
2021-12-06 18:28:35 +01:00
|
|
|
} else if let game = GameType(rawValue: actionString) {
|
2021-12-22 22:13:09 +01:00
|
|
|
result = server.select(game: game, playerToken: token)
|
2021-12-06 18:28:35 +01:00
|
|
|
} else {
|
|
|
|
throw Abort(.badRequest)
|
|
|
|
}
|
|
|
|
switch result {
|
2021-12-01 22:49:54 +01:00
|
|
|
case .success:
|
|
|
|
return ""
|
|
|
|
case .invalidToken:
|
|
|
|
throw Abort(.unauthorized) // 401
|
2021-12-06 18:28:35 +01:00
|
|
|
case .noTableJoined:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
|
|
|
case .tableNotFull:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
|
|
|
case .tableStateInvalid:
|
2021-12-01 22:49:54 +01:00
|
|
|
throw Abort(.preconditionFailed) // 412
|
2021-12-09 11:10:20 +01:00
|
|
|
case .invalidCard:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
2021-12-01 22:49:54 +01:00
|
|
|
}
|
|
|
|
}
|
2021-12-06 11:43:30 +01:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2021-12-22 22:13:09 +01:00
|
|
|
switch server.play(card: card, playerToken: token) {
|
2021-12-06 11:43:30 +01:00
|
|
|
case .success:
|
|
|
|
return ""
|
|
|
|
case .invalidToken:
|
|
|
|
throw Abort(.unauthorized) // 401
|
|
|
|
case .noTableJoined:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
2021-12-09 11:10:20 +01:00
|
|
|
case .tableStateInvalid:
|
2021-12-06 11:43:30 +01:00
|
|
|
throw Abort(.preconditionFailed) // 412
|
|
|
|
case .invalidCard:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
2021-12-09 11:10:20 +01:00
|
|
|
case .tableNotFull:
|
|
|
|
throw Abort(.preconditionFailed) // 412
|
2021-12-06 11:43:30 +01:00
|
|
|
}
|
|
|
|
}
|
2021-11-25 19:15:38 +01:00
|
|
|
}
|