Search code examples
swiftdatabasevapor

Vapor 3 - Trying to map Login request with model that does not exits in database


I am trying to implement a simple login request API. As you can see in the coding sample below, I have LoginRequest which is the data I get from the client (iOS, Android etc). With this I check if User exists in the DB, then I check if the user's password is correct.

However, what I am struggling with is how do I return a model that doesn't exist in the DB. In the example below I have LoginResponse. I don't want to reveal the full User & AuthToken (models in DB) data to the client, hence I have create the LoginResponse model. The only data i want to reveal to the client is the LoginResponse's authToken if everything was successful.

What I expect from the user: username and password

struct LoginRequest : Codable, Content {
    var username: String
    var password : String
}

The response I give back to the user: success (with authToken), incorrect password or user doesn't exists.

/// Response model for user login.
struct LoginResponse : Codable, Content {

    enum LoginResultType : String, Codable {
        case success
        case incorrectPassword
        case noUser
    }

    var authToken : String?
    var state : LoginResultType

    init(state : LoginResultType) {
        authToken  = nil
        self.state = state
    }
}}
func login(_ req : Request) throws -> Future<LoginResponse> {
        return try req.content.decode(LoginRequest.self).flatMap(to: LoginResponse.self) { loginRequest in
            return User.query(on: req).filter(\.username == loginRequest.username)
                .first().map(to: LoginResponse.self) { user in

                    /// check if we have found a user, if not then return LoginResponse with noUser
                    guard let u = user else {
                        return LoginResponse(state: .noUser)
                    }
                    /// if the password isn't the same, then we tell the user
                    /// that the password is incorrect.
                    if u.password != loginRequest.password {
                        return LoginResponse(state: . incorrectPassword)
                    }


                    /// If username and password are the same then we create a random authToken and save this to the DB.
/// Then I need to return LoginResponse with success and authToken.
                    let authToken = AuthToken(token: "<Random number>")
                    authToken.user = u.id
                    return authToken.create(on: req).flatMap(to: LoginResponse.self){ auth in
                        var lr       = LoginResponse(state: .success)
                        lr.authToken = auth.token
                        return lr
                    }
            }
        }
    }

The coding below is a headache, this will not let me return a LoginResponse after I have created a new authToken in the DB.

                    return authToken.create(on: req).flatMap(to: LoginResponse.self){ auth in
                        var lr       = LoginResponse(state: .success)
                        lr.authToken = auth.token
                        return lr
                    }

Once I have created an authToken I want to reveal the authToken in the LoginResponse model with success state, how do I achieve this?


Solution

  • Just use map instead of flatMap.

    map - to return something non-future

    flatMap - to return Future

    And also you could return some object as a future using

    req.eventLoop.newSucceededFuture(result: someObject)
    

    Code below should work like a charm

    func login(_ req : Request) throws -> Future<LoginResponse> {
        return try req.content.decode(LoginRequest.self).flatMap { loginRequest in
            return User.query(on: req).filter(\.username == loginRequest.username).first().flatMap { user in
                        /// check if we have found a user, if not then return LoginResponse with noUser
                guard let u = user else {
                    return req.eventLoop.newSucceededFuture(result: LoginResponse(state: .noUser))
                }
                /// if the password isn't the same, then we tell the user
                /// that the password is incorrect.
                if u.password != loginRequest.password {
                    return req.eventLoop.newSucceededFuture(result: LoginResponse(state: .incorrectPassword))
                }
                /// If username and password are the same then we create a random authToken and save this to the DB.
                /// Then I need to return LoginResponse with success and authToken.
                let authToken = AuthToken(token: "<Random number>")
                authToken.user = u.id
                return authToken.create(on: req).map { auth in
                    var lr = LoginResponse(state: .success)
                    lr.authToken = auth.token
                    return lr
                }
            }
        }
    }
    

    But to be honest it's a bad practice to return errors with 200 OK http code.

    The best practice is to use HTTP codes for errors instead, and Vapor uses this way by design.

    So your code may look simply like this

    struct LoginResponse: Content {
        let authToken: String?
    }
    
    func login(_ req : Request) throws -> Future<LoginResponse> {
        return try req.content.decode(LoginRequest.self).flatMap { loginRequest in
            return User.query(on: req).filter(\.username == loginRequest.username).first().flatMap { user in
                /// check if we have found a user, if not then throw 404
                guard let u = user else {
                    throw Abort(.notFound, reason: "User not found")
                }
                /// if the password isn't the same, then throw 400
                if u.password != loginRequest.password {
                    throw Abort(.badRequest, reason: "Incorrect password")
                }
                /// If username and password are the same then we create a random authToken and save this to the DB.
                /// Then I need to return LoginResponse with success and authToken.
                let authToken = AuthToken(token: "<Random number>")
                authToken.user = u.id
                return authToken.create(on: req).map { auth in
                    var lr = LoginResponse(state: .success)
                    lr.authToken = auth.token
                    return lr
                }
            }
        }
    }
    

    It looks clean and there is no need to parse additional enum on the client side.