diff --git a/Sources/App/Model/Migrations/PasswordResetMigration.swift b/Sources/App/Model/Migrations/PasswordResetMigration.swift new file mode 100644 index 0000000..f0128cf --- /dev/null +++ b/Sources/App/Model/Migrations/PasswordResetMigration.swift @@ -0,0 +1,18 @@ +import FluentSQLiteDriver + +struct PasswordResetMigration: Migration { + + func prepare(on database: FluentSQLiteDriver.Database) -> EventLoopFuture { + database.schema(PasswordReset.schema) + .id() + .field(PasswordReset.Key.user.key, .uuid, .required, .references(User.schema, .id)) + .unique(on: PasswordReset.Key.user.key) + .field(PasswordReset.Key.token.key, .string, .required) + .field(PasswordReset.Key.expiry.key, .date, .required) + .create() + } + + func revert(on database: FluentSQLiteDriver.Database) -> EventLoopFuture { + database.schema(PasswordReset.schema).delete() + } +} diff --git a/Sources/App/Model/PasswordReset.swift b/Sources/App/Model/PasswordReset.swift new file mode 100644 index 0000000..b98e2de --- /dev/null +++ b/Sources/App/Model/PasswordReset.swift @@ -0,0 +1,60 @@ +import Foundation +import FluentSQLiteDriver +import Crypto + +private extension FieldProperty { + convenience init(_ key: PasswordReset.Key) { + self.init(key: key.key) + } +} + +private extension ParentProperty { + convenience init(_ key: PasswordReset.Key) { + self.init(key: key.key) + } +} + +extension PasswordReset.Key { + var key: FieldKey { + .init(stringLiteral: rawValue) + } +} + +final class PasswordReset: Model { + + /// The name of the SQLite table + static let schema = "reset" + + enum Key: String { + case id = "id" + case user = "user" + case token = "token" + case expiry = "expiry" + } + + /// The unique identifier for the reset request + @ID(key: .id) + var id: UUID? + + /// The user associated with the reset request + @Parent(.user) + var user: User + + /// The random reset token issued with the email + @Field(.token) + var resetToken: String + + /// The time when the reset token expires and can't be used anymore + @Field(.expiry) + var expiryDate: Date + + init() { } + + /// Creates a new password reset. + init(id: UUID? = nil, user: User) { + self.id = id + self.user = user + self.resetToken = .newToken() + self.expiryDate = Date().addingTimeInterval(15*60) + } +} diff --git a/Sources/App/Model/User.swift b/Sources/App/Model/User.swift index 7520b80..bbcdae6 100644 --- a/Sources/App/Model/User.swift +++ b/Sources/App/Model/User.swift @@ -52,6 +52,10 @@ final class User: Model { @OptionalParent(.table) var table: Table? + /// The optional password reset request associated with this user + @OptionalChild(for: \.$user) + var resetRequest: PasswordReset? + init() { } /// Creates a new user. diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index 085afca..7e2d4cb 100644 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -28,6 +28,7 @@ public func configure(_ app: Application) throws { app.databases.use(.sqlite(.file(dbFile)), as: .sqlite) } app.migrations.add(UserTableMigration()) + app.migrations.add(PasswordResetMigration()) try app.autoMigrate().wait()