diff --git a/Sources/App/Management/SQLiteDatabase.swift b/Sources/App/Management/SQLiteDatabase.swift index 0058d57..f11b9ed 100644 --- a/Sources/App/Management/SQLiteDatabase.swift +++ b/Sources/App/Management/SQLiteDatabase.swift @@ -114,7 +114,42 @@ final class SQLiteDatabase { /** Change the password of a user with a recovery token + Possible errors: + - `404`: Reset token not found or expired + */ + func updatePassword(password: String, forResetToken token: String, in database: Database) async throws { + // 1. Find and validate the reset request + let reset: PasswordReset? = try await PasswordReset.query(on: database) + .filter(\.$resetToken == token) + .first() + guard let reset else { + throw Abort(.expectationFailed) } + guard reset.expiryDate.timeIntervalSinceNow > 0 else { + throw Abort(.expectationFailed) + } + + // 2. Update the user password + let user = try await reset.$user.get(on: database) + user.passwordHash = password + try await user.save(on: database) + + // 3. Delete the reset request + try await PasswordReset + .query(on: database) + .filter(\.$resetToken == token) + .delete() + } + + func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) async throws -> PasswordHash { + try await User + .query(on: database) + .filter(\.$name == name) + .first() + .unwrap(or: Abort(.notFound)) + .passwordHash + } + 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) diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index c002ba0..38ae36e 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -83,6 +83,29 @@ func requestPlayerPasswordReset(_ app: Application) { 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 + - `404`: Player name not found or no email registered + - `424`: Password could not be hashed + */ +func resetPlayerPasswordWithEmailToken(_ app: Application) { + app.post("player", "reset") { req async throws -> HTTPResponseStatus in + let token = try req.header(.token) + let hash = try req.hashedPassword() // errors: 400, 424 + try await server.updatePassword(password: hash, forResetToken: token, in: req.db) + return .ok + } +} + /** Delete a player.