Schafkopf-Server/Sources/App/routes.swift
2021-12-06 18:28:35 +01:00

329 lines
11 KiB
Swift

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<T>(_ 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 -> 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 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 = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401
}
guard let info = database.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 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 isPublic: Bool
if visibility == "private" {
isPublic = false
} else if visibility == "public" {
isPublic = true
} else {
throw Abort(.badRequest) // 400
}
guard let player = database.registeredPlayerExists(withSessionToken: token) else {
throw Abort(.unauthorized) // 401
}
let table = database.createTable(named: tableName, player: player, isPublic: isPublic)
return try encodeJSON(table)
}
/**
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()
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 -> 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 .success(let table):
return try encodeJSON(table)
case .failure(let result):
switch result {
case .invalidToken:
throw Abort(.unauthorized) // 401
case .alreadyJoinedOtherTable:
throw Abort(.forbidden) // 403
case .tableNotFound:
throw Abort(.gone) // 410
case .tableIsFull:
throw Abort(.expectationFailed) // 417
}
}
}
/**
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 ""
}
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 = Player.Action(rawValue: actionString) {
result = database.performAction(playerToken: token, action: action)
} else if let game = GameType(rawValue: actionString) {
result = database.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
}
}
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 database.play(card: card, playerToken: token) {
case .success:
return ""
case .invalidToken:
throw Abort(.unauthorized) // 401
case .noTableJoined:
throw Abort(.preconditionFailed) // 412
case .invalidTableState:
throw Abort(.preconditionFailed) // 412
case .invalidCard:
throw Abort(.preconditionFailed) // 412
}
}
}