Registration, Login, Resume, Table Creation, Dark Style

This commit is contained in:
Christoph Hagen
2021-11-28 15:53:47 +01:00
parent b87dce55a8
commit 3a1ef01a54
8 changed files with 940 additions and 189 deletions

View File

@ -0,0 +1,14 @@
import Foundation
import Crypto
extension String {
/**
Create a new access token.
*/
static func newToken() -> String {
Crypto.SymmetricKey.init(size: .bits128).withUnsafeBytes {
$0.hexEncodedString()
}
}
}

View File

@ -1,62 +1,68 @@
import Foundation
import Crypto
import Vapor
let playerPerTable = 4
typealias TableId = String
typealias TableName = String
final class Database {
/// A mapping between usernames and their password hashes
private var userPasswordHashes = [String: String]()
private let players: PlayerManagement
/// A mapping between usernames and generated access tokens for a session
private var authTokenForUser = [String: String]()
private let tables: TableManagement
/// A reverse mapping between generated access tokens and usernames
private var userForToken = [String: String]()
/// A list of table ids for public games
private var publicTables = Set<String>()
/// A mapping from table id to table name (for all tables)
private var tableNames = [String: String]()
/// A mapping from table id to participating players
private var tablePlayers = [String: [String]]()
/// A reverse list of players and their table id
private var playerTables = [String: String]()
private var sessions: [SessionToken : WebSocket]
init() {
self.players = PlayerManagement()
self.tables = TableManagement()
self.sessions = [:]
// TODO: Load server data from disk
// TODO: Save data to disk
}
/**
Check if a user exists.
- Parameter name: The name of the user
- Returns: true, if the user exists
*/
func has(user: String) -> Bool {
userPasswordHashes[user] != nil
// MARK: Players & Sessions
func registerPlayer(named name: PlayerName, hash: PasswordHash) -> SessionToken? {
players.registerPlayer(named: name, hash: hash)
}
/**
Get the password hash for a user, if the user exists.
- Parameter name: The name of the user
- Returns: The stored password hash, if the user exists
*/
func hash(ofUser name: String) -> String? {
userPasswordHashes[name]
func passwordHashForExistingPlayer(named name: PlayerName) -> PasswordHash? {
players.passwordHash(ofRegisteredPlayer: name)
}
/**
Create a new user and assign an access token.
- Parameter name: The name of the new user
- Parameter hash: The password hash of the user
- Returns: The generated access token for the session
*/
func add(user name: String, hash: String) -> String {
self.userPasswordHashes[name] = hash
return startSession(forUser: name)
func deletePlayer(named name: PlayerName) {
if let sessionToken = players.deletePlayer(named: name) {
closeAndRemoveSession(for: sessionToken)
}
// TODO: Delete player from tables
}
func isValid(sessionToken token: SessionToken) -> Bool {
players.isValid(sessionToken: token)
}
func startSession(socket: WebSocket, sessionToken: SessionToken) {
closeAndRemoveSession(for: sessionToken)
sessions[sessionToken] = socket
socket.onText { [weak self] socket, text in
self?.didReceive(message: text, forSessionToken: sessionToken)
}
}
private func didReceive(message: String, forSessionToken token: SessionToken) {
// TODO: Handle client requests
print("Session \(token.prefix(6)): \(message)")
}
func endSession(forSessionToken token: SessionToken) {
players.endSession(forSessionToken: token)
closeAndRemoveSession(for: token)
}
private func closeAndRemoveSession(for token: SessionToken) {
_ = sessions.removeValue(forKey: token)?.close()
}
/**
@ -64,32 +70,22 @@ final class Database {
- Parameter name: The user name
- Returns: The generated access token for the session
*/
func startSession(forUser name: String) -> String {
let token = newToken()
self.authTokenForUser[name] = token
self.userForToken[token] = name
return token
func startNewSessionForRegisteredPlayer(named name: PlayerName) -> SessionToken {
players.startNewSessionForRegisteredPlayer(named: name)
}
/**
Get the user for a session token.
- Parameter token: The access token for the user
- Returns: The name of the user, if it exists
*/
func user(forToken token: String) -> String? {
userForToken[token]
func registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
players.registeredPlayerExists(withSessionToken: token)
}
func tableExists(named name: String) -> Bool {
tableNames.contains { $0.value == name }
// MARK: Tables
func tableExists(withId id: TableId) -> Bool {
tables.tableExists(withId: id)
}
func tableExists(withId id: String) -> Bool {
tableNames[id] != nil
}
func tableIsFull(withId id: String) -> Bool {
tablePlayers[id]!.count < playerPerTable
func tableIsFull(withId id: TableId) -> Bool {
tables.tableIsFull(withId: id)
}
/**
@ -99,43 +95,22 @@ final class Database {
- Parameter visible: Indicates that this is a game joinable by everyone
- Returns: The table id
*/
func createTable(named name: String, player: String, visible: Bool) -> String {
let tableId = newToken()
tableNames[tableId] = tableId
tablePlayers[tableId] = [player]
playerTables[player] = tableId
if visible {
publicTables.insert(tableId)
}
return tableId
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
tables.createTable(named: name, player: player, visible: visible)
}
func getPublicTableInfos() -> [TableInfo] {
publicTables.map { tableId in
TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!)
}.sorted()
tables.getPublicTableInfos()
}
func join(tableId: String, player: String) {
tablePlayers[tableId]!.append(player)
if let oldTable = playerTables[tableId] {
remove(player: player, fromTable: oldTable)
}
playerTables[tableId] = tableId
}
func remove(player: String, fromTable tableId: String) {
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
}
/**
Create a new access token.
*/
private func newToken() -> String {
Crypto.SymmetricKey.init(size: .bits128).withUnsafeBytes {
$0.hexEncodedString()
}
func join(tableId: TableId, player: PlayerName) {
let playersAtTable = tables.join(tableId: tableId, player: player)
playersAtTable
.compactMap { players.sessionToken(forPlayer: $0) } // Session Tokens
.compactMap { sessions[$0] } // Sockets
.forEach { socket in
// TODO: Notify sessions about changed players
// socket.send("")
}
}
}

View File

@ -0,0 +1,106 @@
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]()
init() {
}
/**
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
}
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? {
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) {
guard let player = playerNameForToken.removeValue(forKey: token) else {
return
}
sessionTokenForPlayer.removeValue(forKey: 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]
}
}

View File

@ -0,0 +1,75 @@
import Foundation
final class TableManagement {
/// A list of table ids for public games
private var publicTables = Set<TableId>()
/// A mapping from table id to table name (for all tables)
private var tableNames = [TableId: TableName]()
/// A mapping from table id to participating players
private var tablePlayers = [TableId: [PlayerName]]()
/// A reverse list of players and their table id
private var playerTables = [PlayerName: TableId]()
init() {
}
func tableExists(withId id: TableId) -> Bool {
tableNames[id] != nil
}
func tableIsFull(withId id: TableId) -> Bool {
(tablePlayers[id]?.count ?? playerPerTable) < playerPerTable
}
/**
Create a new table with optional players.
- Parameter name: The name of the table
- Parameter players: The player creating the table
- Parameter visible: Indicates that this is a game joinable by everyone
- Returns: The table id
*/
func createTable(named name: TableName, player: PlayerName, visible: Bool) -> TableId {
let tableId = TableId.newToken()
tableNames[tableId] = name
tablePlayers[tableId] = [player]
playerTables[player] = tableId
if visible {
publicTables.insert(tableId)
}
return tableId
}
func getPublicTableInfos() -> [TableInfo] {
publicTables.map { tableId in
TableInfo(id: tableId, name: tableNames[tableId]!, players: tablePlayers[tableId]!)
}.sorted()
}
/**
Join a table.
- Returns: The player names present at the table
*/
func join(tableId: TableId, player: PlayerName) -> [PlayerName] {
guard var players = tablePlayers[tableId] else {
return []
}
players.append(player)
if let oldTable = playerTables[tableId] {
remove(player: player, fromTable: oldTable)
}
tablePlayers[tableId] = players
playerTables[tableId] = tableId
return players
}
func remove(player: PlayerName, fromTable tableId: TableId) {
tablePlayers[tableId] = tablePlayers[tableId]?.filter { $0 != player }
}
}