diff --git a/Sources/App/Management/Configuration.swift b/Sources/App/Management/Configuration.swift index 57796e9..1c9f451 100644 --- a/Sources/App/Management/Configuration.swift +++ b/Sources/App/Management/Configuration.swift @@ -3,6 +3,26 @@ import Foundation struct Configuration { let serverPort: Int + + let mail: EMail + + struct EMail { + + /// The url to the root of the server + let serverDomain: String + + /// SMTP server address + let emailHostname: String + + /// username to login + let email: String + + /// password to login + let password: String + + /// The number of minutes until a password reset token is no longer valid + let tokenExpiryDuration: Int + } } extension Configuration { @@ -27,6 +47,11 @@ extension Configuration { } } } + +extension Configuration.EMail: Codable { + +} + extension Configuration: Codable { } diff --git a/Sources/App/Management/SQLiteDatabase.swift b/Sources/App/Management/SQLiteDatabase.swift index 83c3506..c87a23e 100644 --- a/Sources/App/Management/SQLiteDatabase.swift +++ b/Sources/App/Management/SQLiteDatabase.swift @@ -1,6 +1,7 @@ import Foundation import Fluent import Vapor +import SwiftSMTP typealias PasswordHash = String typealias SessionToken = String @@ -15,8 +16,20 @@ final class SQLiteDatabase { private let tables: TableManagement - init(db: Database) throws { + private let mailConfiguration: Configuration.EMail + + private let smtp: SMTP + + private let mailSender: Mail.User + + init(db: Database, mail: Configuration.EMail) throws { self.tables = try TableManagement(db: db) + 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 } func registerPlayer(named name: PlayerName, hash: PasswordHash, in database: Database) -> EventLoopFuture { @@ -33,6 +46,37 @@ final class SQLiteDatabase { } } + private func sendEmail(name: PlayerName, email: String, token: String) { + let recipient = Mail.User(name: name, email: email) + let url = "\(mailConfiguration.serverDomain)/player/reset?token=\(token)" + 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: + + \(url) + + 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 + """ + ) + + self.smtp.send(mail) { (error) in + if let error = error { + print("Failed to send recovery email to \(email): \(error)") + } + } + } + func passwordHashForExistingPlayer(named name: PlayerName, in database: Database) -> EventLoopFuture { User.query(on: database).filter(\.$name == name).first() .unwrap(or: Abort(.forbidden)).map { $0.passwordHash } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 7e2d4cb..1f6a2e6 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -36,7 +36,7 @@ public func configure(_ app: Application) throws { app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) let db = app.databases.database(.sqlite, logger: .init(label: "Init"), on: app.databases.eventLoopGroup.next())! - server = try SQLiteDatabase(db: db) + server = try SQLiteDatabase(db: db, mail: configuration.mail) // Gracefully shut down by closing potentially open socket DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {