This package makes it easy to handle flows that involves resetting a password. It's up for the consumer to decide how to distribute the token that allows one to reset the password.
Add Reset
to the package dependencies (in your Package.swift
file):
dependencies: [
...,
.package(url: "https://github.com/nodes-vapor/reset.git", from: "1.0.0")
]
as well as to your target (e.g. "App"):
targets: [
...
.target(
name: "App",
dependencies: [... "Reset" ...]
),
...
]
Next, copy/paste the Resources/Views/Reset
folder into your project in order to be able to use the provided Leaf files. These files can be changed as explained in the Specifying the responses section, however it's recommended to copy this folder to your project anyway. This makes it easier for you to keep track of updates and your project will work if you decide later on to not use your own customized leaf files.
First make sure that you've imported Reset everywhere it's needed:
import Reset
Reset comes with a light-weight provider that we'll need to register in the configure
function in our configure.swift
file:
try services.register(ResetProvider<User>(config: ResetConfig(
name: AppConfig.app.name,
baseURL: AppConfig.app.url,
signer: ExpireableJWTSigner(
expirationPeriod: 3600, // 1 hour
signer: .hs256(
key: env(EnvironmentKey.Reset.signerKey, "secret-reset"
).convertToData())
)
))
)
Please see Making a PasswordResettable
model for more information on confirming a type to PasswordResettable
.
Make sure to add the relevant Reset routes, e.g. in your configure.swift or routes.swift:
services.register(Router.self) { container -> EngineRouter in
let router = EngineRouter.default()
try router.useResetRoutes(User.self, on: container)
return router
}
This package comes with a small Leaf tag that is used to pass Reset-related information such as project name and project url to Leaf. To add it to your project, please do the following:
public func configure(_ config: inout Config, _ env: inout Environment, _ services: inout Services) throws {
services.register { _ -> LeafTagConfig in
var tags = LeafTagConfig.default()
tags.useResetLeafTags()
return tags
}
}
There's a couple of things that needs to be in place for conforming your model to PasswordResettable
. The following example is based on having a User
model which you would like to add support for resetting a password.
The first thing to define is the data that is needed in order to request a reset password flow and the data for actually resetting the password. It could look like this:
extension User: PasswordResettable {
// ...
public struct RequestReset: RequestCreatable, Decodable, HasReadableUsername {
static let readableUsernameKey = \RequestReset.username
public let username: String
}
public struct ResetPassword: RequestCreatable, Decodable, HasReadablePassword {
static let readablePasswordKey = \ResetPassword.password
public let password: String
}
// ..
}
Basically the username (this could also be the email) is needed to request a reset flow and a new password is needed to submit the password change.
Note how that
RequestReset
conforms toHasReadableUsername
. This enables Reset to implement thefind
method for looking up the user automatically.
Once the user has requested to reset their password, the sendPasswordReset
function will be called. The implementation could send the url by email or just include the token in a text message. It's up to the implementer to decide how to distribute this.
Here's an example using the Mailgun package to send out an email with the reset password url:
extension User: PasswordResettable {
// ...
public func sendPasswordReset(
url: String,
token: String,
expirationPeriod: TimeInterval,
context: ResetPasswordContext,
on req: Request
) throws -> Future<Void> {
let mailgun = try req.make(Mailgun.self)
let expire = Int(expirationPeriod / 60) // convert to minutes
return try req
.make(LeafRenderer.self)
.render(ViewPath.Reset.resetPasswordEmail, ["url": url, "expire": expire])
.map(to: String.self) { view in
String(bytes: view.data, encoding: .utf8) ?? ""
}
.map(to: Mailgun.Message.self) { html in
Mailgun.Message(
from: "donotreply@reset.com",
to: self.email,
subject: "Reset password",
text: "Please turn on html to view this email.",
html: html
)
}
.flatMap(to: Response.self) { message in
try mailgun.send(message, on: req)
}
.transform(to: ())
}
// ..
}
There might be cases where you would want to have multiple signers for multiple different reset password flows. One example could be to handle the regular reset password flow as well as automatically resetting a password when a user gets created. By implementing the signer
function, you're able to handle this:
extension User: PasswordResettable {
// ...
public enum MyResetPasswordContext: HasRequestResetPasswordContext {
case userRequestedToResetPassword
case newUserWithoutPassword
public static func requestResetPassword() -> MyResetPasswordContext {
return .userRequestedToResetPassword
}
}
public func signer(
for context: MyResetPasswordContext,
on container: Container
) throws -> ExpireableJWTSigner {
let resetConfig: ResetConfig<User> = try container.make() // The default signer
let myConfig: MyConfig = try container.make() // Some project specific config that holds the extra signer
switch context {
case .userRequestedToResetPassword: return resetConfig.signer
case .newUserWithoutPassword: return myConfig.newUserSetPasswordSigner
}
}
// ..
}
Please note that you need to implement your own
Context
if you want to handle multiple signers.
All endpoints and responses that Reset uses can be overwritten. Reset provides responses for the following cases:
- Form for requesting a reset password flow
- Response for letting the user know that the reset password url has been sent
- Form for resetting the password
- Response for letting the user know that the password has been reset
Here's a small example where the request to reset password should only be exposed through the API:
let customResponse = ResetResponses(
resetPasswordRequestForm: { req in
return try HTTPResponse(status: .notFound).encode(for: req)
},
resetPasswordUserNotified: { req in
return try HTTPResponse(status: .noContent).encode(for: req)
},
resetPasswordForm: { req, user in
return try req
.make(LeafRenderer.self)
.render("MyPathForShowingResetForm")
.encode(for: req)
},
resetPasswordSuccess: { req, user in
return try req
.make(LeafRenderer.self)
.render("MyPathForShowingResetPasswordSuccess")
.encode(for: req)
}
)
This instance can then be used when registering the provider as explained in Adding the Provider.
Alternatively, instead of passing in ResetResponses
in the ResetConfig
, one could pass in their own implementation of ResetControllerType
for full customizability.
This package is developed and maintained by the Vapor team at Nodes.
This package is open-sourced software licensed under the MIT license