import Foundation typealias PlayerName = String typealias PasswordHash = String typealias SessionToken = String /// Manages player registration, session tokens and password hashes final class PlayerManagement { /// A mapping between player name and their password hashes private var playerPasswordHashes = [PlayerName: PasswordHash]() /// A mapping between player name and generated access tokens for a session private var sessionTokenForPlayer = [PlayerName: SessionToken]() /// A reverse mapping between generated access tokens and player name private var playerNameForToken = [SessionToken: PlayerName]() private let passwordFile: FileHandle private let passwordFileUrl: URL init(storageFolder: URL) throws { let url = storageFolder.appendingPathComponent("passwords.txt") if !FileManager.default.fileExists(atPath: url.path) { try Data().write(to: url) } passwordFile = try FileHandle(forUpdating: url) passwordFileUrl = url if #available(macOS 10.15.4, *) { guard let data = try passwordFile.readToEnd() else { try passwordFile.seekToEnd() return } try loadPasswords(data: data) } else { let data = passwordFile.readDataToEndOfFile() try loadPasswords(data: data) } print("Loaded \(playerPasswordHashes.count) players") } private func loadPasswords(data: Data) throws { String(data: data, encoding: .utf8)! .components(separatedBy: "\n") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { $0 != "" } .forEach { line in let parts = line.components(separatedBy: ":") // Token may contain the separator guard parts.count >= 2 else { print("Invalid line in password file") return } let name = parts[0] let token = parts.dropFirst().joined(separator: ":") if token == "" { playerPasswordHashes[name] = nil } else { playerPasswordHashes[name] = token } } } private func save(password: PasswordHash, forPlayer player: PlayerName) -> Bool { let entry = player + ":" + password + "\n" let data = entry.data(using: .utf8)! do { if #available(macOS 10.15.4, *) { try passwordFile.write(contentsOf: data) } else { passwordFile.write(data) } try passwordFile.synchronize() return true } catch { print("Failed to save password to disk: \(error)") return false } } private func deletePassword(forPlayer player: PlayerName) -> Bool { save(password: "", forPlayer: player) } /** Check if a player exists. - Parameter name: The name of the player - Returns: true, if the player exists */ func hasRegisteredPlayer(named user: PlayerName) -> Bool { playerPasswordHashes[user] != nil } /** Get the password hash for a player, if the player exists. - Parameter name: The name of the player - Returns: The stored password hash, if the player exists */ func passwordHash(ofRegisteredPlayer name: PlayerName) -> PasswordHash? { playerPasswordHashes[name] } /** Create a new player and assign an access token. - Parameter name: The name of the new player - Parameter hash: The password hash of the player - Returns: The generated access token for the session */ func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? { guard !hasRegisteredPlayer(named: name) else { return nil } guard save(password: hash, forPlayer: name) else { return nil } self.playerPasswordHashes[name] = hash return startNewSessionForRegisteredPlayer(named: name) } /** Delete a player - Parameter name: The name of the player to delete. - Returns: The session token of the current player, if one exists */ func deletePlayer(named name: PlayerName) -> SessionToken? { guard deletePassword(forPlayer: name) else { return nil } playerPasswordHashes.removeValue(forKey: name) guard let sessionToken = sessionTokenForPlayer.removeValue(forKey: name) else { return nil } playerNameForToken.removeValue(forKey: sessionToken) return sessionToken } func isValid(sessionToken token: SessionToken) -> Bool { playerNameForToken[token] != nil } func sessionToken(forPlayer player: PlayerName) -> SessionToken? { sessionTokenForPlayer[player] } /** Start a new session for an existing player. - Parameter name: The player name - Returns: The generated access token for the session */ func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken { let token = SessionToken.newToken() self.sessionTokenForPlayer[name] = token self.playerNameForToken[token] = name return token } func endSession(forSessionToken token: SessionToken) -> PlayerName? { guard let player = playerNameForToken.removeValue(forKey: token) else { return nil } sessionTokenForPlayer.removeValue(forKey: player) return player } /** Get the player for a session token. - Parameter token: The access token for the player - Returns: The name of the player, if it exists */ func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? { playerNameForToken[token] } }