Schafkopf-Server/Sources/App/Model/PlayerManagement.swift
2021-12-01 12:45:42 +01:00

180 lines
5.9 KiB
Swift

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]
}
}