3

我想创建一条路线让用户更新他们的数据(例如更改他们的电子邮件或用户名)。为了确保用户不能使用与另一个用户相同的用户名,我想检查数据库中是否已经存在具有相同用户名的用户。

我已经使用户名在迁移中唯一。

我有一个看起来像这样的用户模型:

struct User: Content, SQLiteModel, Migration {
    var id: Int?
    var username: String
    var name: String
    var email: String
    var password: String

    var creationDate: Date?

    // Permissions
    var staff: Bool = false
    var superuser: Bool = false

    init(username: String, name: String, email: String, password: String) {
        self.username = username
        self.name = name
        self.email = email
        self.password = password
        self.creationDate = Date()
    }
}

这是我要使用它的一段代码:

func create(_ req: Request) throws -> EventLoopFuture<User> {
    return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in

        // Check if `userRequest.email` already exists
        // If if does -> throw Abort(.badRequest, reason: "Email already in use")
        // Else -> Go on with creation

        let digest = try req.make(BCryptDigest.self)
        let hashedPassword = try digest.hash(userRequest.password)
        let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)

        return persistedUser.save(on: req)
    }
}

我可以这样做(请参阅下一个片段),但这似乎是一个奇怪的选择,因为当必须执行更多检查(例如,在更新用户的情况下)时,它需要大量嵌套。

func create(_ req: Request) throws -> EventLoopFuture<User> {
    return try req.content.decode(UserCreationRequest.self).flatMap { userRequest in
        let userID = userRequest.email
        return User.query(on: req).filter(\.userID == userID).first().flatMap { existingUser in
            guard existingUser == nil else {
                throw Abort(.badRequest, reason: "A user with this email already exists")
            }

            let digest = try req.make(BCryptDigest.self)
            let hashedPassword = try digest.hash(userRequest.password)
            let persistedUser = User(name: userRequest.name, email: userRequest.email, password: hashedPassword)

            return persistedUser.save(on: req)
        }
    }
}

正如其中一个答案所建议的那样,我尝试添加错误中间件(请参阅下一个片段),但这并没有正确捕获错误(也许我在代码中做错了 - 刚从 Vapor 开始)。

import Vapor
import FluentSQLite

enum InternalError: Error {
    case emailDuplicate
}

struct EmailDuplicateErrorMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
        let response: Future<Response>

        do {
            response = try next.respond(to: request)
        } catch is SQLiteError {
            response = request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
        }

        return response.catchFlatMap { error in
            if let response = error as? ResponseEncodable {
                do {
                    return try response.encode(for: request)
                } catch {
                    return request.eventLoop.newFailedFuture(error: InternalError.emailDuplicate)
                }
            } else {
                return request.eventLoop.newFailedFuture(error: error)
            }
        }
    }
}
4

2 回答 2

3

快速的方法是User.query(on: req).filter(\.email == email).count()在尝试保存之前执行类似的操作并检查是否等于 0。

然而,虽然这对几乎每个人都适用,但您仍然会冒两个用户尝试在完全相同的时间使用相同用户名注册的极端情况 - 处理此问题的唯一方法是捕获保存失败,检查是否是因为电子邮件的唯一约束并将错误返回给用户。但是,即使对于大型应用程序,您实际命中的机会也很少。

于 2019-10-19T20:33:22.070 回答
1

我会unique使用以下方法在模型中创建字段Migration

extension User: Migration {
  static func prepare(on connection: SQLiteConnection) -> Future<Void> {
    return Database.create(self, on: connection) { builder in
      try addProperties(to: builder)
      builder.unique(on: \.email)
    }
  }
}

如果您使用默认值String作为 的字段类型email,那么您将需要减少它,因为这会创建一个对于键VARCHAR(255)来说太大的字段。UNIQUE然后,我会使用一些自定义Middleware来捕获使用同一电子邮件进行第二次尝试保存记录时出现的错误。

struct DupEmailErrorMiddleware: Middleware
{
    func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response>
    {
        let response: Future<Response>
        do {
            response = try next.respond(to: request)
        } catch is MySQLError {
            // needs a bit more sophistication to check the specific error
            response = request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
        }
        return response.catchFlatMap
        {
            error in
            if let response = error as? ResponseEncodable
            {
                do
                {
                    return try response.encode(for: request)
                }
                catch
                {
                    return request.eventLoop.newFailedFuture(error: InternalError.dupEmail)
                }
            } else
            {
                return request.eventLoop.newFailedFuture(error: error   )
            }
        }
    }
}

编辑:

您的自定义错误需要类似于:

enum InternalError: Debuggable, ResponseEncodable
{
    func encode(for request: Request) throws -> EventLoopFuture<Response>
    {
        let response = request.response()
        let eventController = EventController()
        //TODO make this return to correct view
        eventController.message = reason
        return try eventController.index(request).map
        {
            html in
            try response.content.encode(html)
            return response
        }
    }

    case dupEmail

    var identifier:String
    {
        switch self
        {
            case .dupEmail: return "dupEmail"
        }
    }

    var reason:String
    {
       switch self
       {
            case .dupEmail: return "Email address already used"
        }
    }
}

在上面的代码中,通过在控制器中设置一个值向用户显示实际错误,然后在视图中拾取该值并显示警报。此方法允许通用错误处理程序负责显示错误消息。但是,在您的情况下,您可能只需要在catchFlatMap.

于 2019-10-20T10:02:09.443 回答