重置 🏳

Swift Version Vapor Version Circle CI codebeat badge codecov Readme Score GitHub license

此包可以轻松处理涉及重置密码的流程。如何分发允许重置密码的令牌由消费者决定。

📦 安装

Reset 添加到包依赖项中 (在您的 Package.swift 文件中)

dependencies: [
    ...,
    .package(url: "https://github.com/nodes-vapor/reset.git", from: "1.0.0")
]

以及您的目标 (例如 “App”)

targets: [
    ...
    .target(
        name: "App",
        dependencies: [... "Reset" ...]
    ),
    ...
]

接下来,将 Resources/Views/Reset 文件夹复制/粘贴到您的项目中,以便能够使用提供的 Leaf 文件。这些文件可以按照指定响应部分中的说明进行更改,但建议无论如何都将此文件夹复制到您的项目中。这使您可以更轻松地跟踪更新,并且如果您稍后决定不使用您自己的自定义 leaf 文件,您的项目也将正常工作。

开始使用 🚀

首先确保您已在所有需要的地方导入了 Reset

import Reset

添加 Provider

Reset 附带一个轻量级的 provider,我们需要在 configure.swift 文件中的 configure 函数中注册它

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())
        )
    ))
)

有关确认类型符合 PasswordResettable 的更多信息,请参阅制作 PasswordResettable 模型

添加 Reset 路由

确保添加相关的 Reset 路由,例如在您的 configure.swift 或 routes.swift 中

services.register(Router.self) { container -> EngineRouter in
    let router = EngineRouter.default()
    try router.useResetRoutes(User.self, on: container)
    return router
}

添加 Leaf 标签

此包附带一个小的 Leaf 标签,用于将 Reset 相关信息(例如项目名称和项目 URL)传递给 Leaf。要将其添加到您的项目中,请执行以下操作

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
    }
}

制作 PasswordResettable 模型

要使您的模型符合 PasswordResettable,需要进行一些设置。以下示例基于您想要添加重置密码支持的 User 模型。

请求和重置结构体

首先要定义的是请求重置密码流程所需的数据以及实际重置密码的数据。它可能看起来像这样

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
    }

    // ..
}

基本上,需要用户名(也可以是电子邮件)来请求重置流程,并且需要新密码来提交密码更改。

请注意 RequestReset 如何符合 HasReadableUsername。这使 Reset 能够实现 find 方法来自动查找用户。

发送重置密码 URL

一旦用户请求重置密码,将调用 sendPasswordReset 函数。该实现可以通过电子邮件发送 URL,或者只是在短信中包含令牌。如何分发此内容由实施者决定。

这是一个使用 Mailgun 包发送包含重置密码 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: ())
    }

    // ..
}

处理多个重置流程

在某些情况下,您可能希望为多个不同的重置密码流程设置多个签名者。一个例子可能是处理常规的重置密码流程,以及在创建用户时自动重置密码。通过实现 signer 函数,您可以处理这种情况

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
        }
    }

    // ..
}

请注意,如果您想处理多个签名者,则需要实现您自己的 Context

指定响应

Reset 使用的所有端点和响应都可以被覆盖。Reset 为以下情况提供响应

这是一个小例子,其中请求重置密码应仅通过 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)
    }
)

然后可以在注册 provider 时使用此实例,如添加 Provider中所述。

或者,除了在 ResetConfig 中传入 ResetResponses 之外,还可以传入您自己实现的 ResetControllerType 以获得完全的自定义性。

🏆 鸣谢

此包由 Nodes 的 Vapor 团队开发和维护。

📄 许可证

此包是根据 MIT 许可证获得许可的开源软件