Vapor 3 - Как проверить наличие похожих писем перед сохранением объекта - PullRequest
1 голос
/ 19 октября 2019

Я хотел бы создать маршрут, чтобы пользователи могли обновлять свои данные (например, менять свою электронную почту или имя пользователя). Чтобы убедиться, что пользователь не может использовать то же имя пользователя, что и другой пользователь, я хотел бы проверить, существует ли в базе данных пользователь с таким же именем пользователя.

Я уже сделал имя пользователя уникальным в миграциях.

У меня есть пользовательская модель, которая выглядит следующим образом:

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

В качестве одного из предложенных ответов я попытался добавить промежуточное программное обеспечение Error (см. Следующий фрагмент), но это не позволяет правильно отловить ошибку (возможно, я делаю что-то не так в коде - только что начал с 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)
            }
        }
    }
}

Ответы [ 2 ]

1 голос
/ 20 октября 2019

Я бы сделал поле 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.

1 голос
/ 19 октября 2019

Быстрый способ сделать это - сделать что-то вроде User.query(on: req).filter(\.email == email).count() и проверить, что равно 0, прежде чем пытаться сохранить.

Однако, хотя это будет работать почти для всех, вы все равно рискуете крайними случаями, когдадва пользователя пытаются зарегистрироваться с одним и тем же именем пользователя в одно и то же время - единственный способ справиться с этим - это отловить ошибку сохранения, проверить, не было ли это из-за уникального ограничения на электронную почту, и вернуть ошибку пользователю. Однако шансы на то, что вы действительно попадете, довольно редки, даже для больших приложений.

...