Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition - Early Acess 1 · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

21. Validation
Written by Tim Condon

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Note: This update is an early-access release. This chapter has not yet been updated to Vapor 4.

In the previous chapters, you built a fully-functional API and website. Users can send requests and fill in forms to create acronyms, categories and other users. In this chapter, you’ll learn how to use Vapor’s Validation library to verify some of the information users send the application. You’ll create a registration page on the website for users to sign up. Finally, you’ll validate the data from this form and display an error message if the data isn’t correct.

The registration page

Create a new file in Resources/Views called register.leaf. This is the template for the registration page. Open register.leaf and add the following:

#set("content") {
  <h1>#(title)</h1>

  <form method="post">
    <div class="form-group">
      <label for="name">Name</label>
      <input type="text" name="name" class="form-control"
       id="name"/>
    </div>

    <div class="form-group">
      <label for="username">Username</label>
      <input type="text" name="username" class="form-control"
       id="username"/>
    </div>

    <div class="form-group">
      <label for="password">Password</label>
      <input type="password" name="password"
       class="form-control" id="password"/>
    </div>

    <div class="form-group">
      <label for="confirmPassword">Confirm Password</label>
      <input type="password" name="confirmPassword"
       class="form-control" id="confirmPassword"/>
    </div>

    <button type="submit" class="btn btn-primary">
      Register
    </button>
  </form>
}

#embed("base")

This is very similar to the templates for creating an acronym and logging in. The template contains four input fields for:

  • name
  • username
  • password
  • password confirmation

Save the file. Next, open WebsiteController.swift and at the bottom of the file add the following context for the registration page:

struct RegisterContext: Encodable {
  let title = "Register"
}

Next, below logoutHandler(_:), add the following route handler for the registration page:

func registerHandler(_ req: Request) throws -> Future<View> {
  let context = RegisterContext()
  return try req.view().render("register", context)
}

Like the other routes handlers, this creates a context then calls render(_:_:) to render register.leaf.

Next, at the bottom of WebsiteController.swift, create the Content for the POST request for registration:

struct RegisterData: Content {
  let name: String
  let username: String
  let password: String
  let confirmPassword: String
}

This Content type matches the expected data received from the registration POST request. The variables match the names of the inputs in register.leaf. Next, add the following after registerHandler(_:) to create a route handler for this POST request:

// 1
func registerPostHandler(
  _ req: Request,
  data: RegisterData
) throws -> Future<Response> {
  // 2
  let password = try BCrypt.hash(data.password)
  // 3
  let user = User(
    name: data.name,
    username: data.username,
    password: password)
  // 4
  return user.save(on: req).map(to: Response.self) { user in
    // 5
    try req.authenticateSession(user)
    // 6
    return req.redirect(to: "/")
  }
}

Here’s what’s going on in the route handler:

  1. Define a route handler that accepts a request and the decoded RegisterData.

  2. Hash the password submitted to the form.

  3. Create a new User, using the data from the form and the hashed password.

  4. Save the new user and unwrap the returned future.

  5. Authenticate the session for the new user. This automatically logs users in when they register, thereby providing a nice user experience when signing up with the site.

  6. Return a redirect back to the home page.

Next, in boot(router:) add the following below authSessionRoutes.post("logout", use: logoutHandler):

// 1
authSessionRoutes.get("register", use: registerHandler)
// 2
authSessionRoutes.post(RegisterData.self, at: "register",
                       use: registerPostHandler)

Here’s what this does:

  1. Connect a GET request for /register to registerHandler(_:).
  2. Connect a POST request for /register to registerPostHandler(_:data:). Decode the request’s body to RegisterData.

Finally, open base.leaf. Before the closing </ul> in the navigation bar, add the following:

#// 1
#if(!userLoggedIn) {
  #// 2
  <li class="nav-item #if(title == "Register"){active}">
    #// 3
    <a href="/register" class="nav-link">Register</a>
  </li>
}

Here’s what the new Leaf code does:

  1. Check to see if there’s a logged in user. You only want to display the register link if there’s no user logged in.
  2. Add a new navigation link to the navigation bar. Set the active class if the current page is the Register page.
  3. Add a link to the new /register route.

Save the template then build and run the project in Xcode. Visit http://localhost:8080 in your browser. You’ll see the new navigation link:

Click Register and you’ll see the new register page:

If you fill out the form and click Register, the app takes you to the home page. Notice the Log out button in the top right; this confirms that registration automatically logged you in.

Basic validation

Vapor provides a validation module to help you check data and models. Open WebsiteController, and add the following at the bottom:

// 1
extension RegisterData: Validatable, Reflectable {
  // 2
  static func validations() throws
    -> Validations<RegisterData> {
    // 3
    var validations = Validations(RegisterData.self)
    // 4
    try validations.add(\.name, .ascii)
    // 5
    try validations.add(\.username,
                        .alphanumeric && .count(3...))
    // 6
    try validations.add(\.password, .count(8...))
    // 7
    return validations
  }
}
do {
  try data.validate()
} catch {
  return req.future(req.redirect(to: "/register"))
}

Custom validation

If you’ve been following closely, you’ll notice a flaw in the validation: Nothing ensures the passwords match! Vapor’s validation library doesn’t provide a built-in way to check that two strings match. However, it’s easy to add custom validators. In the validations() for RegisterData, before return validations, add the following:

// 1
validations.add("passwords match") { model in
  // 2
  guard model.password == model.confirmPassword else {
    // 3
    throw BasicValidationError("passwords don’t match")
  }
}

Displaying an error

Currently, when a user fills out the form incorrectly, the application redirects back to the form with no indication anything went wrong. Open register.leaf and add the following under <h1>#(title)</h1>:

#if(message) {
  <div class="alert alert-danger" role="alert">
    Please fix the following errors:<br />
    #(message)
  </div>
}
let message: String?

init(message: String? = nil) {
  self.message = message
}
let context = RegisterContext()
let context: RegisterContext
if let message = req.query[String.self, at: "message"] {
  context = RegisterContext(message: message)
} else {
  context = RegisterContext()
}
catch (let error) {
  let redirect: String
  if let error = error as? ValidationError,
    let message = error.reason.addingPercentEncoding(
      withAllowedCharacters: .urlQueryAllowed) {
    redirect = "/register?message=\(message)"
  } else {
    redirect = "/register?message=Unknown+error"
  }
  return req.future(req.redirect(to: redirect))
}

Where to go from here?

In this chapter, you learned how to use Vapor’s validation library to check request’s data. You can apply validation to models and other types as well.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now