diff --git a/Public/api.js b/Public/api.js index b74a49e..7f099c4 100644 --- a/Public/api.js +++ b/Public/api.js @@ -10,6 +10,8 @@ const headerKeyPassword = "password"; const headerKeyToken = "token"; const headerKeyName = "name"; const headerKeyMail = "email"; +const headerKeyVisibility = "visibility"; +const headerKeyAction = "action"; function webSocketPath() { const prefix = (window.location.protocol === "https:") ? "wss://" : "ws://" @@ -29,29 +31,50 @@ async function performRegisterPlayerRequest(name, password, email) { } async function performDeletePlayerRequest(name, password) { - return fetch(apiPath + "/player/delete/" + name, { method: 'POST', body: password }) + return fetch(apiPath + "/player/delete", { + method: 'POST', + headers: { + [headerKeyName]: name, + [headerKeyPassword]: password, + } + }) .then(convertServerResponse) .then(function(value) {}) } async function performLoginPlayerRequest(name, password) { - return fetch(apiPath + "/player/login/" + name, { method: 'POST', body: password }) + return fetch(apiPath + "/player/login", { + method: 'POST', + headers: { + [headerKeyName]: name, + [headerKeyPassword]: password, + } + }) .then(convertServerResponse) } async function performLogoutRequest(token) { - return fetch(apiPath + "/player/logout", { method: 'POST', body: token }) + return fetch(apiPath + "/player/logout", { + method: 'POST', + headers: { [headerKeyToken]: token }, + }) .then(convertServerResponse) .then(function(value) {}) } async function resumeSessionRequest(token) { - return fetch(apiPath + "/player/resume", { method: 'POST', body: token }) + return fetch(apiPath + "/player/resume", { + method: 'POST', + headers: { [headerKeyToken]: token }, + }) .then(convertServerResponse) } async function performGetCurrentTableRequest(token) { - return fetch(apiPath + "/player/table", { method: 'POST', body: token }) + return fetch(apiPath + "/player/table", { + method: 'POST', + headers: [headerKeyToken]: token, + }) .then(convertServerResponse) .then(convertJsonResponse) } @@ -65,7 +88,7 @@ async function performRecoveryEmailRequest(name) { } async function performResetPasswordRequest(token, password) { - return fetch(apiPath + "/player/reset", { + return fetch(apiPath + "/player/password/new", { method: 'POST', headers: { [headerKeyPassword] : password, [headerKeyToken] : token } }) @@ -74,36 +97,68 @@ async function performResetPasswordRequest(token, password) { async function performCreateTableRequest(token, name, visibility) { const vis = visibility ? "public" : "private"; - return fetch(apiPath + "/table/create/" + vis + "/" + name, { method: 'POST', body: token }) + return fetch(apiPath + "/table/create", { + method: 'POST', + headers: { + [headerKeyVisibility]: vis, + [headerKeyToken]: token, + [headerKeyName]: name, + } + }) .then(convertServerResponse) .then(convertJsonResponse) } async function performJoinTableRequest(tableId, token) { - return fetch(apiPath + "/table/join/" + tableId, { method: 'POST', body: token }) + return fetch(apiPath + "/table/join", { + method: 'POST', + headers: { + [headerKeyName]: tableId, + [headerKeyToken]: token, + } + }) .then(convertServerResponse) .then(convertJsonResponse) } async function performGetPublicTablesRequest(token) { - return fetch(apiPath + "/tables/public", { method: 'POST', body: token }) + return fetch(apiPath + "/tables/public", { + method: 'POST', + headers: { [headerKeyToken] : token } + }) .then(convertServerResponse) .then(convertJsonResponse) } async function performLeaveTableRequest(token) { - return fetch(apiPath + "/table/leave", { method: 'POST', body: token }) + return fetch(apiPath + "/table/leave", { + method: 'POST', + headers: { [headerKeyToken] : token }, + }) .then(convertServerResponse) .then(function(value) {}) } async function performPlayerActionRequest(token, action) { - return fetch(apiPath + "/player/action/" + action, { method: 'POST', body: token }) + return fetch(apiPath + "/player/action", { + method: 'POST', + headers: { + [headerKeyToken] : token, + [headerKeyAction] : action, + }, + }) .then(convertServerResponse) + .then(function(value) {}) } async function performPlayCardRequest(token, card) { - return fetch(apiPath + "/player/card/" + card, { method: 'POST', body: token }) + return fetch(apiPath + "/player/card", { + method: 'POST', + headers: { + [headerKeyToken] : token, + [headerKeyAction] : card, + }, + }) .then(convertServerResponse) .then(function(value) {}) } diff --git a/Sources/App/API/HeaderKey.swift b/Sources/App/API/HeaderKey.swift index c637283..2a5d0ed 100644 --- a/Sources/App/API/HeaderKey.swift +++ b/Sources/App/API/HeaderKey.swift @@ -6,6 +6,8 @@ enum HeaderKey: String { case password case email case token + case visibility + case action } extension Request { diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 7484263..a316c32 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -98,7 +98,7 @@ func requestPlayerPasswordReset(_ app: Application) { - `424`: Password could not be hashed */ func resetPlayerPasswordWithEmailToken(_ app: Application) { - app.post("player", "reset") { req async throws -> HTTPResponseStatus in + 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 @@ -109,21 +109,20 @@ func resetPlayerPasswordWithEmailToken(_ app: Application) { /** 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 + **Headers** + - `name`: The name of the player + - `password`: The password of the player - Possible errors: + **Possible errors** - `400`: Missing name or password - `401`: 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 - } + 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 @@ -135,22 +134,24 @@ func deletePlayer(_ app: Application) { /** 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 - Possible errors: + **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 - - Returns: The session token for the user + **Response** + - `body`: 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 - } + 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 @@ -159,19 +160,23 @@ func loginPlayer(_ app: Application) { } } - /** - 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 - */ +/** + 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") { req -> String in - guard let token = req.body.string else { - throw Abort(.badRequest) // 400 - } + 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 } @@ -179,38 +184,43 @@ func resumeSession(_ app: Application) { } } - /** - 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 - */ +/** + 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") { req -> String in - guard let token = req.body.string else { - throw Abort(.badRequest) // 400 - } + app.post("player", "logout") { request -> HTTPResponseStatus in + let token = try request.header(.token) + server.endSession(forSessionToken: token) - return "" + return .ok } } - /** - 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 - */ +/** + 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") { req -> String in - guard let token = req.body.string else { - throw Abort(.badRequest) // 400 - } + 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 } @@ -221,11 +231,11 @@ func getTableForPlayer(_ app: Application) { } } - /** - 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. - */ +/** + 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 @@ -237,24 +247,29 @@ func openWebsocket(_ app: Application) { } } - // MARK: Tables +// 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 - */ +/** + 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", ":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 - } + 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 @@ -276,18 +291,19 @@ func createTable(_ app: Application) { List the public tables. **Headers** - - `token`: The session token of the player, as a string in the request body + - `token`: The session token of the player **Possible errors** - `400`: Missing token - `401`: The session token is invalid - - Returns: A JSON object with a list of public tables (id, name, player list) + + **Response** + - `body`: 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 - } + app.post("tables", "public") { request -> String in + let token = try request.header(.token) + guard server.isValid(sessionToken: token) else { throw Abort(.unauthorized) // 401 } @@ -296,24 +312,25 @@ func getPublicTables(_ app: Application) { } } - /** - 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 - */ +/** + 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", ":table") { request -> String in - guard let string = request.parameters.get("table"), - let table = UUID(uuidString: string), - let token = request.body.string else { + 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) @@ -321,30 +338,42 @@ func joinTable(_ app: Application) { } } - /** - 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 - */ +/** + 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 - guard let token = request.body.string else { - throw Abort(.badRequest) - } + 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", ":action") { req -> String in - guard let token = req.body.string, - let actionString = req.parameters.get("action") else { - throw Abort(.badRequest) - } + 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) @@ -355,41 +384,54 @@ func performActionForPlayer(_ app: Application) { } switch result { case .success: - return "" + return .ok case .invalidToken: - throw Abort(.unauthorized) // 401 + return .unauthorized // 401 case .noTableJoined: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .tableNotFull: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .tableStateInvalid: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .invalidCard: - throw Abort(.preconditionFailed) // 412 + 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", ":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) - } + app.post("player", "card") { request -> 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 server.play(card: card, playerToken: token) { case .success: - return "" + return .ok case .invalidToken: - throw Abort(.unauthorized) // 401 + return .unauthorized // 401 case .noTableJoined: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .tableStateInvalid: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .invalidCard: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 case .tableNotFull: - throw Abort(.preconditionFailed) // 412 + return .preconditionFailed // 412 } } }