2021-12-22 22:11:37 +01:00
|
|
|
import Foundation
|
|
|
|
import Fluent
|
|
|
|
import Vapor
|
2022-10-11 12:09:43 +02:00
|
|
|
import SwiftSMTP
|
2021-12-22 22:11:37 +01:00
|
|
|
|
|
|
|
typealias PasswordHash = String
|
|
|
|
typealias SessionToken = String
|
|
|
|
|
|
|
|
final class SQLiteDatabase {
|
|
|
|
|
|
|
|
/// 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 tables: TableManagement
|
|
|
|
|
2022-10-11 12:09:43 +02:00
|
|
|
private let mailConfiguration: Configuration.EMail
|
|
|
|
|
|
|
|
private let smtp: SMTP
|
|
|
|
|
|
|
|
private let mailSender: Mail.User
|
|
|
|
|
|
|
|
init(db: Database, mail: Configuration.EMail) throws {
|
2021-12-22 22:11:37 +01:00
|
|
|
self.tables = try TableManagement(db: db)
|
2022-10-11 12:09:43 +02:00
|
|
|
self.smtp = SMTP(
|
|
|
|
hostname: mail.emailHostname,
|
|
|
|
email: mail.email,
|
|
|
|
password: mail.password)
|
|
|
|
self.mailSender = Mail.User(name: "Schafkopf Server", email: mail.email)
|
|
|
|
self.mailConfiguration = mail
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
2022-10-12 14:34:43 +02:00
|
|
|
func registerPlayer(named name: PlayerName, hash: PasswordHash, email: String?, in database: Database) async throws -> SessionToken {
|
|
|
|
if let email {
|
|
|
|
let user = try await User
|
|
|
|
.query(on: database)
|
|
|
|
.filter(\.$recoveryEmail == email)
|
|
|
|
.first()
|
|
|
|
if user != nil {
|
|
|
|
throw Abort(.conflict)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
2022-10-12 14:34:43 +02:00
|
|
|
}
|
|
|
|
if try await User.query(on: database).filter(\.$name == name).first() != nil {
|
|
|
|
throw Abort(.conflict)
|
|
|
|
}
|
|
|
|
let user = User(name: name, hash: hash, email: email)
|
|
|
|
try await user.create(on: database)
|
|
|
|
|
|
|
|
// Create a new token and store it for the user
|
|
|
|
let token = SessionToken.newToken()
|
|
|
|
self.sessionTokenForPlayer[name] = token
|
|
|
|
self.playerNameForToken[token] = name
|
|
|
|
return token
|
|
|
|
}
|
2022-10-12 19:30:11 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
Send a password reset email.
|
|
|
|
|
|
|
|
Possible errors:
|
2022-10-12 19:43:16 +02:00
|
|
|
- `417`: Player name or email not found.
|
2022-10-12 19:30:11 +02:00
|
|
|
*/
|
|
|
|
func sendPasswordResetEmailIfPossible(name: PlayerName, in database: Database) async throws {
|
|
|
|
guard let user = try await User.query(on: database).filter(\.$name == name).first() else {
|
2022-10-12 19:43:16 +02:00
|
|
|
throw Abort(.expectationFailed)
|
2022-10-12 19:30:11 +02:00
|
|
|
}
|
|
|
|
guard let email = user.recoveryEmail else {
|
2022-10-12 19:43:16 +02:00
|
|
|
throw Abort(.expectationFailed)
|
2022-10-12 19:30:11 +02:00
|
|
|
}
|
|
|
|
try await user.$resetRequest.load(on: database)
|
|
|
|
if let request = user.resetRequest {
|
|
|
|
request.renew()
|
|
|
|
try await request.save(on: database)
|
|
|
|
self.sendEmail(name: name, email: email, token: request.resetToken)
|
|
|
|
} else {
|
|
|
|
let reset = PasswordReset()
|
|
|
|
try await user.$resetRequest.create(reset, on: database)
|
|
|
|
self.sendEmail(name: name, email: email, token: reset.resetToken)
|
|
|
|
}
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
2022-10-11 12:09:43 +02:00
|
|
|
private func sendEmail(name: PlayerName, email: String, token: String) {
|
|
|
|
let recipient = Mail.User(name: name, email: email)
|
2022-10-12 19:30:11 +02:00
|
|
|
let url = "\(mailConfiguration.serverDomain)/recovery.html?token=\(token)"
|
2022-10-11 12:09:43 +02:00
|
|
|
let mail = Mail(
|
|
|
|
from: mailSender,
|
|
|
|
to: [recipient],
|
|
|
|
subject: "Schafkopf Server Password Reset",
|
|
|
|
text:
|
|
|
|
"""
|
|
|
|
Hello \(name),
|
|
|
|
|
|
|
|
a reset of your account password has been requested for the Schafkopf Server at \(mailConfiguration.serverDomain).
|
|
|
|
To choose a new password, click the following link:
|
|
|
|
|
2022-10-12 19:30:11 +02:00
|
|
|
\(url)
|
2022-10-11 12:09:43 +02:00
|
|
|
|
|
|
|
The link will expire in \(mailConfiguration.tokenExpiryDuration) minutes. If you didn't request the password reset, you don't have to do anything.
|
|
|
|
|
|
|
|
Regards,
|
|
|
|
|
|
|
|
The Schafkopf Server Team
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
2022-10-12 19:30:11 +02:00
|
|
|
smtp.send(mail) { (error) in
|
2022-10-11 12:09:43 +02:00
|
|
|
if let error = error {
|
|
|
|
print("Failed to send recovery email to \(email): \(error)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
/**
|
|
|
|
Change the password of a user with a recovery token
|
2021-12-22 22:11:37 +01:00
|
|
|
|
2022-10-12 19:30:43 +02:00
|
|
|
Possible errors:
|
2022-10-12 19:43:16 +02:00
|
|
|
- `417`: Reset token not found or expired
|
2022-10-12 19:30:43 +02:00
|
|
|
*/
|
|
|
|
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)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
2022-10-12 19:30:43 +02:00
|
|
|
|
|
|
|
// 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()
|
2022-10-12 19:43:16 +02:00
|
|
|
.unwrap(or: Abort(.unauthorized))
|
2022-10-12 19:30:43 +02:00
|
|
|
.passwordHash
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
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()
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func isValid(sessionToken token: SessionToken) -> Bool {
|
|
|
|
playerNameForToken[token] != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func startSession(socket: WebSocket, sessionToken token: SessionToken) -> Bool {
|
|
|
|
guard let player = playerNameForToken[token] else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return tables.connect(player: player, using: socket)
|
|
|
|
}
|
|
|
|
|
|
|
|
func endSession(forSessionToken sessionToken: SessionToken) {
|
|
|
|
guard let player = endExistingSession(forSessionToken: sessionToken) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tables.disconnect(player: player)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func endExistingSession(forSessionToken token: SessionToken) -> PlayerName? {
|
|
|
|
guard let player = playerNameForToken.removeValue(forKey: token) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
sessionTokenForPlayer.removeValue(forKey: player)
|
|
|
|
return player
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
Start a new session for an existing user.
|
|
|
|
- Parameter name: The user 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 registeredPlayerExists(withSessionToken token: SessionToken) -> PlayerName? {
|
|
|
|
playerNameForToken[token]
|
|
|
|
}
|
|
|
|
|
|
|
|
func currentTableOfPlayer(named player: PlayerName) -> TableInfo? {
|
|
|
|
tables.tableInfo(player: player)
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
private func points(for player: PlayerName, in database: Database) async throws -> Int {
|
|
|
|
try await User.query(on: database)
|
2021-12-22 22:11:37 +01:00
|
|
|
.filter(\.$name == player)
|
|
|
|
.first()
|
|
|
|
.unwrap(or: Abort(.notFound))
|
2022-10-12 19:28:28 +02:00
|
|
|
.points
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
private func user(named name: PlayerName, in database: Database) async throws -> User {
|
|
|
|
try await User
|
|
|
|
.query(on: database)
|
2021-12-22 22:11:37 +01:00
|
|
|
.filter(\.$name == name)
|
|
|
|
.first()
|
|
|
|
.unwrap(or: Abort(.notFound))
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
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)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Tables
|
|
|
|
|
|
|
|
/**
|
|
|
|
Create a new table with optional players.
|
|
|
|
- Parameter name: The name of the table
|
|
|
|
- Parameter players: The player creating the table
|
|
|
|
- Parameter isPublic: Indicates that this is a game joinable by everyone
|
|
|
|
- Returns: The table id
|
|
|
|
*/
|
2022-10-12 19:28:28 +02:00
|
|
|
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)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func getPublicTableInfos() -> [PublicTableInfo] {
|
|
|
|
tables.publicTableList
|
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
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)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
2022-10-12 19:28:28 +02:00
|
|
|
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)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func performAction(playerToken: SessionToken, action: PlayerAction) -> PlayerActionResult {
|
|
|
|
guard let player = playerNameForToken[playerToken] else {
|
|
|
|
return .invalidToken
|
|
|
|
}
|
|
|
|
return tables.performAction(player: player, action: action)
|
|
|
|
}
|
|
|
|
|
|
|
|
func select(game: GameType, playerToken: SessionToken) -> PlayerActionResult {
|
|
|
|
guard let player = playerNameForToken[playerToken] else {
|
|
|
|
return .invalidToken
|
|
|
|
}
|
|
|
|
return tables.select(game: game, player: player)
|
|
|
|
}
|
|
|
|
|
2022-10-18 11:40:08 +02:00
|
|
|
func play(card: Card, playerToken: SessionToken, in database: Database) async throws -> PlayerActionResult {
|
2021-12-22 22:11:37 +01:00
|
|
|
guard let player = playerNameForToken[playerToken] else {
|
|
|
|
return .invalidToken
|
|
|
|
}
|
2022-10-18 11:40:08 +02:00
|
|
|
return try await tables.play(card: card, player: player, in: database)
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|
2022-01-24 17:15:11 +01:00
|
|
|
|
|
|
|
func disconnectAllSockets() {
|
|
|
|
tables.disconnectAllSockets()
|
|
|
|
}
|
2021-12-22 22:11:37 +01:00
|
|
|
}
|