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?
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.