Vapor 3 - How to check for similar email before saving object

给你一囗甜甜゛ 提交于 2021-01-27 14:52:12

问题


I would like to create a route to let users update their data (e.g. changing their email or their username). To make sure a user cannot use the same username as another user, I would like to check if a user with the same username already exists in the database.

I have already made the username unique in the migrations.

I have a user model that looks like this:

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

This is the piece of code where I want to use it:

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

I could do it like this (see next snippet) but it seems a strange option as it requires a lot of nesting when more checks for e.g. uniqueness would have to be performed (for instance in the case of updating a user).

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

As one of the answers suggested I've tried to add Error middleware (see next snippet) but this does not correctly catch the error (maybe I am doing something wrong in the code - just started with 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)
            }
        }
    }
}

回答1:


I would make the field unique in the model using a Migration such as:

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

If you use a default String as the field type for email, then you will need to reduce it as this creates a field VARCHAR(255) which is too big for a UNIQUE key. I would then use a bit of custom Middleware to trap the error that arises when a second attempt to save a record is made using the same email.

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

EDIT:

Your custom error needs to be something like:

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

In the code above, the actual error is displayed to the user by setting a value in the controller, which is then picked up in the view and an alert displayed. This method allows a general-purpose error handler to take care of displaying the error messages. However, in your case, it might be that you could just create the response in the catchFlatMap.




回答2:


The quick way of doing it is to do something like User.query(on: req).filter(\.email == email).count() and check that equals 0 before attempting the save.

However, whilst this will work fine for almost everyone, you still risk edge cases where two users try to register with the same username at the exact same time - the only way to handle this is to catch the save failure, check if it was because the unique constraint on the email and return the error to the user. However the chances of you actually hitting that are pretty rare, even for big apps.



来源:https://stackoverflow.com/questions/58466181/vapor-3-how-to-check-for-similar-email-before-saving-object

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!