diff --git a/Sources/App/Extensions/Optional+Extensions.swift b/Sources/App/Extensions/Optional+Extensions.swift new file mode 100644 index 0000000..0f66670 --- /dev/null +++ b/Sources/App/Extensions/Optional+Extensions.swift @@ -0,0 +1,18 @@ +// +// File.swift +// +// +// Created by CH on 12.10.22. +// + +import Foundation + +extension Optional { + + func unwrap(or error: @autoclosure () -> Error) throws -> Wrapped { + guard let unwrapped = self else { + throw error() + } + return unwrapped + } +} diff --git a/Sources/App/Management/SQLiteDatabase.swift b/Sources/App/Management/SQLiteDatabase.swift index 0204848..50cf9c1 100644 --- a/Sources/App/Management/SQLiteDatabase.swift +++ b/Sources/App/Management/SQLiteDatabase.swift @@ -87,17 +87,14 @@ final class SQLiteDatabase { } } - func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) -> EventLoopFuture { - User.query(on: database).filter(\.$name == name).first() - .unwrap(or: Abort(.forbidden)).map { $0.passwordHash } - } + /** + Change the password of a user with a recovery token - func deletePlayer(named name: PlayerName, in database: Database) -> EventLoopFuture { - user(named: name, in: database).flatMap { user in - self.tables.leaveTable(player: user, in: database) - }.flatMap { - User.query(on: database).filter(\.$name == name).delete() } + func deletePlayer(named name: PlayerName, in database: Database) async throws { + let user = try await user(named: name, in: database) + try await tables.leaveTable(player: user, in: database) + try await User.query(on: database).filter(\.$name == name).delete() } func isValid(sessionToken token: SessionToken) -> Bool { @@ -147,29 +144,25 @@ final class SQLiteDatabase { tables.tableInfo(player: player) } - private func points(for player: PlayerName, in database: Database) -> EventLoopFuture { - User.query(on: database) + private func points(for player: PlayerName, in database: Database) async throws -> Int { + try await User.query(on: database) .filter(\.$name == player) .first() .unwrap(or: Abort(.notFound)) - .map { $0.points } + .points } - private func user(named name: PlayerName, in database: Database) -> EventLoopFuture { - User.query(on: database) + private func user(named name: PlayerName, in database: Database) async throws -> User { + try await User + .query(on: database) .filter(\.$name == name) .first() .unwrap(or: Abort(.notFound)) } - private func user(withToken token: SessionToken, in database: Database) -> EventLoopFuture { - database.eventLoop - .future() - .map { self.playerNameForToken[token] } - .unwrap(or: Abort(.unauthorized)) - .flatMap { name in - self.user(named: name, in: database) - } + private func user(withToken token: SessionToken, in database: Database) async throws -> User { + let name = try playerNameForToken[token].unwrap(or: Abort(.unauthorized)) + return try await user(named: name, in: database) } // MARK: Tables @@ -181,26 +174,23 @@ final class SQLiteDatabase { - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) -> EventLoopFuture { - user(named: player, in: database).flatMap { player in - self.tables.createTable(named: name, player: player, isPublic: isPublic, in: database) - } + func createTable(named name: TableName, player: PlayerName, isPublic: Bool, in database: Database) async throws -> TableInfo { + let user = try await user(named: player, in: database) + return try await tables.createTable(named: name, player: user, isPublic: isPublic, in: database) } func getPublicTableInfos() -> [PublicTableInfo] { tables.publicTableList } - func join(tableId: UUID, playerToken: SessionToken, in database: Database) -> EventLoopFuture { - user(withToken: playerToken, in: database).flatMap { player in - self.tables.join(tableId: tableId, player: player, in: database) - } + func join(tableId: UUID, playerToken: SessionToken, in database: Database) async throws -> TableInfo { + let user = try await user(withToken: playerToken, in: database) + return try await tables.join(tableId: tableId, player: user, in: database) } - func leaveTable(playerToken: SessionToken, in database: Database) -> EventLoopFuture { - user(withToken: playerToken, in: database).flatMap { player in - self.tables.leaveTable(player: player, in: database) - } + func leaveTable(playerToken: SessionToken, in database: Database) async throws { + let user = try await user(withToken: playerToken, in: database) + try await tables.leaveTable(player: user, in: database) } func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult { diff --git a/Sources/App/Management/TableManagement.swift b/Sources/App/Management/TableManagement.swift index 586c380..d2afe80 100644 --- a/Sources/App/Management/TableManagement.swift +++ b/Sources/App/Management/TableManagement.swift @@ -38,19 +38,14 @@ final class TableManagement { - Parameter isPublic: Indicates that this is a game joinable by everyone - Returns: The table id */ - func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) -> EventLoopFuture { + func createTable(named name: TableName, player: User, isPublic: Bool, in database: Database) async throws -> TableInfo { let table = Table(name: name, isPublic: isPublic) - return table.create(on: database).flatMap { - player.$table.id = table.id - return player.update(on: database) - }.flatMap { - Table.query(on: database).with(\.$players).filter(\.$id == table.id!).first() - }.unwrap(or: Abort(.notFound)) - .map { storedTable in - let table = WaitingTable(newTable: storedTable) - self.tables[table.id] = table - return table.tableInfo(forPlayer: player.name) - } + try await table.create(on: database) + player.$table.id = table.id + try await player.update(on: database) + let waitingTable = WaitingTable(newTable: table) + self.tables[waitingTable.id] = waitingTable + return waitingTable.tableInfo(forPlayer: player.name) } /// A list of all public tables @@ -77,51 +72,51 @@ final class TableManagement { - Parameter player: The name of the player who wants to join. - Returns: The result of the join operation */ - func join(tableId: UUID, player: User, in database: Database) -> EventLoopFuture { - return database.eventLoop.future().flatMapThrowing { _ -> ManageableTable in - if let existing = self.currentTable(for: player.name) { - guard existing.id == tableId else { - throw Abort(.forbidden) // 403 - } - return existing + func join(tableId: UUID, player: User, in database: Database) async throws -> TableInfo { + let table = try joinableTable(for: player, id: tableId) + player.$table.id = table.id + try await player.update(on: database) + table.sendUpdateToAllPlayers() + return table.tableInfo(forPlayer: player.name) + } + + private func joinableTable(for player: User, id tableId: UUID) throws -> ManageableTable { + if let existing = self.currentTable(for: player.name) { + guard existing.id == tableId else { + throw Abort(.forbidden) // 403 } - guard let table = self.tables[tableId] else { - throw Abort(.gone) // 410 - } - guard let joinableTable = table as? WaitingTable, - joinableTable.add(player: player.name, points: player.points) else { - throw Abort(.expectationFailed) // 417 - } - return joinableTable - }.flatMap { table -> EventLoopFuture in - player.$table.id = table.id - return player.update(on: database).map { table } - }.map { table in - table.sendUpdateToAllPlayers() - return table.tableInfo(forPlayer: player.name) + return existing } + guard let table = self.tables[tableId] else { + throw Abort(.gone) // 410 + } + guard let joinableTable = table as? WaitingTable, + joinableTable.add(player: player.name, points: player.points) else { + throw Abort(.expectationFailed) // 417 + } + return joinableTable } /** A player leaves the table it previously joined - Parameter player: The player leaving the table */ - func leaveTable(player: User, in database: Database) -> EventLoopFuture { + func leaveTable(player: User, in database: Database) async throws { guard let oldTable = currentTable(for: player.name) else { - return database.eventLoop.makeSucceededVoidFuture() + return } player.$table.id = nil guard let table = WaitingTable(oldTable: oldTable, removing: player.name) else { tables[oldTable.id] = nil - return player.update(on: database).flatMap { - Table.query(on: database).filter(\.$id == oldTable.id).delete() - } + try await player.update(on: database) + try await Table.query(on: database).filter(\.$id == oldTable.id).delete() + return } /// `player.canStartGame` is automatically set to false, because table is not full tables[table.id] = table table.sendUpdateToAllPlayers() // TODO: Update points for all players - return player.update(on: database) + try await player.update(on: database) } func connect(player: PlayerName, using socket: WebSocket) -> Bool { diff --git a/Sources/App/Model/PasswordReset.swift b/Sources/App/Model/PasswordReset.swift index b98e2de..e7757e2 100644 --- a/Sources/App/Model/PasswordReset.swift +++ b/Sources/App/Model/PasswordReset.swift @@ -48,13 +48,25 @@ final class PasswordReset: Model { @Field(.expiry) var expiryDate: Date - init() { } + init() { + self.resetToken = .newToken() + self.expiryDate = Self.currentExpiryDate() + } + + func renew() { + self.resetToken = .newToken() + self.expiryDate = Self.currentExpiryDate() + } /// Creates a new password reset. init(id: UUID? = nil, user: User) { self.id = id - self.user = user + self.$user.id = user.id! self.resetToken = .newToken() - self.expiryDate = Date().addingTimeInterval(15*60) + self.expiryDate = Self.currentExpiryDate() + } + + private static func currentExpiryDate() -> Date { + Date().addingTimeInterval(15*60) } } diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index d6aecb4..058ea84 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -76,17 +76,17 @@ func registerPlayer(_ app: Application) { - Returns: Nothing */ func deletePlayer(_ app: Application) { - app.post("player", "delete", ":name") { req -> EventLoopFuture in - guard let name = req.parameters.get("name"), - let password = req.body.string else { - throw Abort(.badRequest) // 400 + 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 } - 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 { "" } + 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 } } @@ -101,17 +101,16 @@ func deletePlayer(_ app: Application) { - Returns: The session token for the user */ func loginPlayer(_ app: Application) { - app.post("player", "login", ":name") { req -> EventLoopFuture in - guard let name = req.parameters.get("name"), - let password = req.body.string else { + 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 } - 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) - } + 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) } } @@ -205,10 +204,10 @@ func openWebsocket(_ app: Application) { - 401: The session token is invalid */ func createTable(_ app: Application) { - app.post("table", "create", ":visibility", ":name") { req -> EventLoopFuture in - guard let visibility = req.parameters.get("visibility"), - let tableName = req.parameters.get("name"), - let token = req.body.string else { + 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 @@ -223,8 +222,8 @@ func createTable(_ app: Application) { guard let player = server.registeredPlayerExists(withSessionToken: token) else { throw Abort(.unauthorized) // 401 } - return server.createTable(named: tableName, player: player, isPublic: isPublic, in: req.db) - .flatMapThrowing(encodeJSON) + let result = try await server.createTable(named: tableName, player: player, isPublic: isPublic, in: request.db) + return try encodeJSON(result) } } @@ -263,14 +262,14 @@ func getPublicTables(_ app: Application) { */ func joinTable(_ app: Application) { - app.post("table", "join", ":table") { req -> EventLoopFuture in - guard let string = req.parameters.get("table"), + app.post("table", "join", ":table") { request -> String in + guard let string = request.parameters.get("table"), let table = UUID(uuidString: string), - let token = req.body.string else { + let token = request.body.string else { throw Abort(.badRequest) } - return server.join(tableId: table, playerToken: token, in: req.db) - .flatMapThrowing(encodeJSON) + let result = try await server.join(tableId: table, playerToken: token, in: request.db) + return try encodeJSON(result) } } @@ -283,11 +282,12 @@ func joinTable(_ app: Application) { - Returns: Nothing */ func leaveTable(_ app: Application) { - app.post("table", "leave") { req -> EventLoopFuture in - guard let token = req.body.string else { + app.post("table", "leave") { request -> HTTPResponseStatus in + guard let token = request.body.string else { throw Abort(.badRequest) } - return server.leaveTable(playerToken: token, in: req.db).map { "" } + try await server.leaveTable(playerToken: token, in: request.db) + return .ok } }