Extending the application: brief requirements

With that, it's time to put it all in practice and extend our application. The requirements for this extension are simple. We want to:

  1. Restrict the /albums/recently-added routes (both the GET and POST) to only authenticated users.
  2. We want to give the user the option for us to remember their username for future authentications.
  3. We want to have a logout link that kills the user's session, and then prohibits them from gaining access to restricted routes until they re-authenticate.

If we break this down, there are a few work items for us. We need to create a login form that we'll use to get the user's credentials to authenticate, which should take the place of item 1. We can also satisfy item 2 by putting a classic Remember my username checkbox on the authentication form as well. Finally, we'll need to put a logout link somewhere on our site. No problemo! Let's get cracking!

Creating the login form

We're going to build the login form first because, hey, we need to get the credentials somehow and a phone call isn't going to cut it. For now, it's probably easiest if we just create the login Selmer template next to the signup form template we created in Chapter 7, Getting Started with the Database. We can do the following:

  1. Create a new file, resources/templates/login.html.
  2. Fill the file with the following:
    {% extends "templates/base.html" %}  <!-- 1 -->
    {% block content %}
    <h1>Login. <span class="small">That last session was so lame.</span></h1>
    <div class="row">
      <div class="col-md-6">
        {% if invalid-credentials? %}  <!-- 2 -->
        <p class="errors">The provided username and/or password are incorrect.
        </p>
        {% endif %}
        <form role="form" method="POST" action="/login">
          <div class="form-group">
            <label for="username">Username</label>
            <input type="text" name="username"
              class="form-control" id="username"
              placeholder="AtticusButch">
          </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">
          </div>
          <button type="submit"
            class="btn btn-default">Submit</button>
        </form>
      </div>
    </div>
    {% endblock %}

The form looks pretty similar to our signup form. We're extending our base template (at <!-- 1 -->), and we have a username and password field that we'll be POSTing to /login. The last thing we're doing is conditionally rendering a Wrong username/password message if the :invalid-credentials? key is on the context (<!-- 2 -->).

Of course we need to be able to render this form, so let's create a new route for the /login URI. The /signup route is handled in the hipstr.routes.home namespace, so we'll put the /login route there as well. In the hipstr.routes.home namespace, do the following:

  1. Add a route to the home-routes that manages the GET method for /login:
    (GET "/login" [] (login-page))
  2. Add the hipstr.routes.home/login-page route helper function. This will simply render the login page:
    (defn login-page
      "Renders the login form."
      []
      (layout/render "login.html"))

That should be all there is to it. We can now fire up a browser and navigate to http://localhost:3000/login, and we should see something similar to the following:

Creating the login form

Next, let's lock down the /albums/recently-added routes.

Restricting the recently-added route

As we saw earlier in the chapter, restricting access to routes is a two-step process. We need to A, mark the route as restricted, and B, tell Luminus how to grant a access to the restricted route. Let's first restrict the route.

Restricting the route

In the hipstr.routes.albums namespace, add the following requirement:

(:require …
          [noir.util.route :refer [restricted]])

This imports the restricted macro that we'll use to restrict access to the 2 routes in question. To do this, we'll modify the GET and POST routes for /albums/recently-added as restricted:

(defroutes album-routes
  (GET "/albums/recently-added" [] (restricted (recently-added-page)))
  (POST "/albums/recently-added" [& album-form]
    (restricted (recently-added-submit album-form)))
  …

That's all there is to restricting access to a route, as far as the route is concerned. However, now we have to tell Luminus when not to restrict access to the route. And for that, we'll need to create a new access rule in our app handler.

Checking if the user is authenticated

I typically like to keep things partitioned a bit. We need to put some code somewhere that checks if a user is authenticated. For now the most natural place for that is in our hipstr.models.user-model namespace (or someplace else, if you prefer).

Let's create the hipstr.models.user-model/is-authed? function first, to check if a request is authenticated. This function will simply check if there's a :user_id in the session for the current request.

First, bring the noir.session into the hipstr.models.user-model namespace:

…
(:require …
              [noir.session :as session]
…)

Secondly, add the following function to hipstr.models.user-model:

(defn is-authed?
  "Returns false if the current request is anonymous; otherwise
  true."
  [_] ;#1
  (not (nil? (session/get :user_id))))

The substance of this function is simple. The only thing to note is that we don't care about the parameter (#1). Every access rule is passed the request map, but we're more concerned about the session, so we just ignore it.

Defining the access rule

The last thing we need to do is define the access rule. We know that, if our application were to grow big and strong, we may define several access rules governing various routes. So, instead of cluttering up the hipstr.handler namespace, we'll create a new hipstr.routes.access namespace and define our rules there.

  1. Create a new namespace, hipstr.routes.access, and give it access to our new is-authed? function:
    (ns hipstr.routes.access
      (:require [hipstr.models.user-model :refer [is-authed?]])
  2. Create a def called rules that returns a vector of access-rule maps. Our vector will only contain a single rule, which defers validation to the is-authed? function:
    (def rules
      "The rules for accessing various routes in our application."
      [{:redirect "/login" :rule is-authed?}])
  3. Finally, in our hipstr.handler/app application handler, set our :access-rules to the rules vector we just defined:
    (def app (app-handler
              …
             :access-rules access/rules
             …))

    Tip

    Don't forget to :require the hipstr.access namespace at the top!

With that, any anonymous request to a restricted route will automatically be redirected to the /login route, where our gorgeous login form will be presented to the user. We can test our route restriction by trying to navigate directly to http://localhost:3000/albums/recently-added, which should automatically redirect us to /login.

Note

If the access rule fails to redirect you, try restarting the Ring Server and then hitting the restricted route again.

Now that we have a restricted route and a login form, let's move on to authenticating the user.

Authenticating the user

The form POST will do 3 things. First, it will try and validate the user's credentials. There's no need for us to validate the format of the credentials coming up the pipe, because we already ensured they're in the appropriate format when we put them in the database as part of our Signup page. So the credentials on the login form will either match something in the database or they won't. Secondly, if the set of credentials fail to validate, then we'll re-render the login page and tell the user that their username/password was incorrect (that is, make use of that invalid-credentials context value the form is currently expecting). Finally, if the credentials successfully validate, we'll redirect the user to the /albums/recently-added route we had previously locked down.

Validating the credentials

Since our hipstr.models.user-model has the is-authed? function, it makes sense to put an auth-user function beside it. The function will grab a user from the database matching the provided username and, if it exists, will check if the passwords match. However, you'll recall in Chapter 7, Getting Started with the Database that we store a hashed version of the password, so we'll need to make use of the crypto.password.bcrypt namespace (the same namespace we used to originally hash the password for storage). If the username and password match, we'll return the user map from the database; otherwise we'll return nil (including if a user with the provided username does not exist).

First, we'll need to create a simple SQL query that fetches a user by username. Add the following in our users.sql file:

-- name: get-user-by-username
-- Fetches a user from the DB based on username.
SELECT *
FROM users
WHERE username=:username

The preceding query will be processed by YeSQL at runtime, resulting in a get-user-by-username function in our namespace. Next, add the following function to the hipstr.models.user-model:

(defn auth-user
  "Validates a username/password and, if they match, adds the user_id to the session and returns the user map from the database. Otherwise nil."
  [username password]
  (let [user (first (get-user-by-username
                    {:username username}))]              ;#1
    (when (and user (password/check password
                    (:password user)))                   ;#2
      (session/put! :user_id (:user_id user))          ;#3
      (dissoc user  :password))))                          ;#4

The auth-user function makes use of the YeSQL-generated get-user-by-username function (#1), which, if successful, will return a map of matching users. Since our users.username database field has a unique index constraint on it, we know that only 0 or 1 result will be returned in the vector, hence the call to first. We then make use of the crypto.password.bcrypt/check function (#2), which returns true if the hashed password matches an unhashed password. If the user exists and the passwords match, we then stuff the :user_id into the session (#3), and then return the user map – but without the password (#4), as there's no need to proliferate that throughout our app.

If all is happy, then the :user_id will be added to the user's session, which is what our access rule checks for all restricted routes. The last thing we need to do to authenticate the user is to handle that pesky form POST.

Handling the form POST

We'll overload the hipstr.routes.home/login-page to accept a different arity of arguments, specifically the login form's value map. Extend it to the following:

(defn login-page
  ([]
    (layout/render "login.html" {:username (cookies/remember-me)}))
  ([credentials]
    (if (apply u/auth-user (map credentials [:username :password]))
      (response/redirect "/albums/recently-added"))
      (layout/render "login.html" {:invalid-credentials? true}))))

The overloaded function is pretty simple: If the user successfully authenticates with the username and password, then redirect them to the restricted route /albums/recently-added. Otherwise, re-render the login form with the invalid-credentials set to true. The last thing to do before we try out the login form is to create the POST /login route. Add the following to the home-routes:

(POST "/login" [& login-form] (login-page login-form))

Try it out for yourself! If we provide an incorrect username or password, our login form is re-rendered with a foreboding blood-red error message:

Handling the form POST

And when we provide a valid username/password, we're redirected to the /albums/recently-added page:

Handling the form POST

But typing in our username is so boring! I have a thousand websites and a thousand usernames, so let's take care of action item number 2 in our requirements and add that remember me cookie.

Writing the "Remember Me" cookie

Over the years, I've become rather disdainful of random cookie code littered throughout a web application. I prefer to keep cookies in a rather central location. That way, when we want to change the way a cookie behaves we only have to do it once instead of a gazillion times. For that reason, create another namespace, hipstr.cookies, and throw the following in there:

(ns hipstr.cookies
  (:require [noir.cookies :as c]))

(defn remember-me
  ([]
    "Gets the username in the remember-me cookie."
    (c/get :remember-me))
  ([username]
    "Sets a remember-me cookie to the user's browser with the
	user's username."
    (if username
    (c/put! :remember-me {:value username
                          :path "/"
                          :max-age (* 60 60 24 365)})
    (c/put! :remember-me {:value "" :path "/" :max-age -1}))))

The hisptr.cookies acts as a kind of business wrapper to the noir.cookies namespace. In this namespace, we put an overloaded remember-me function, which will either get or set the value of the remember-me cookie, depending on whether or not it's called with a value. You'll notice in the overloaded function that, if we call remember-me with a falsey value (nil or "") that we'll basically delete the cookie.

To make use of the remember-me cookie, let's extend our login form to include a simple checkbox. Back in the resources/templates/login.html, add the following between the password field and the submit button:

<div class="form-group">
  <label for="password">Password</label>
  <input type="password" name="password" class="form-control" id="password">
</div>
<div class="form-group">
  <input type="checkbox" name="remember-me"
  {% if username %} checked{% endif %}>
  Remember me on this computer
</div>
<button type="submit" class="btn btn-default">Submit</button>

All we did here was add a simple HTML checkbox. Yep. Living on the edge! The checkbox will now be marked checked if there's a username on the context map. Let's also set the value of the username text field to the username context value:

<input type="text" name="username" class="form-control" id="username" placeholder="AtticusButch" value="{{ username }}">

Next we'll modify our hipstr.routes.home/login-page to take the remember-me cookie and checkbox into account. First, import the hipstr.cookies namespace:

(:require …
          [hipstr.cookies :as cookies]
          …)

Next, we'll set the username context value when rendering the login page:

([]
(layout/render "login.html" {:username (cookies/remember-me)})

Finally, if the user has checked the remember-me checkbox, and they successfully authenticate on POST, we'll write their username to the remember-me cookie, otherwise we'll set the cookie to nil (essentially deleting it):

(if (apply u/auth-user (map credentials [:username :password]))
  (do (if (:remember-me credentials)
        (cookies/remember-me (:username credentials))
        (cookies/remember-me ""))
      (response/redirect "/albums/recently-added")))

With all that, we should now see a checkbox on the login form. The first time we view it, it will be unchecked. However, if we check it and successfully authenticate, and then go back to the login form, you'll notice that it will be checked and our username will be pre-populated in the username text field, shown as follows:

Writing the "Remember Me" cookie

Finally, the last thing we need to do is create the logout route and link to it.

Creating the logout route

Our logout link will be simple. Any form of navigation (GET, POST, etc.) to /logout will invalidate the user's authenticated status and redirect back to /. Since our is-authed? and auth-user functions are in hisptr.models.user-model, we will add a third method alongside them, called invalidate-auth:

(defn invalidate-auth
  "Invalidates a user's current authenticated state."
  []
  (session/clear!))

That's it. We'll just blow away anything in the session because, hey, why not! Technically, if all we wanted to do was prohibit the user from accessing restricted routes, we could have simply called (session/remove! :user_id), and that would have sufficed. But for now, there are no business rules keeping us from blowing everything away – also, it frees up some memory.

Next, we'll add the route and the helper function. Add the following function and route to our hipstr.routes.home namespace:

(defn logout []
  "Logs the user out of this session."
  (u/invalidate-auth)
  (response/redirect "/"))

(defroutes home-routes
  …
  (ANY "/logout" [] (logout))
  …)

Finally, we need to get a Logout link in there. Since we're extending base.html for all of our templates, that seems like the best place to put it. This allows us to put the logout link in the top-right corner of every page. Open the templates/base.html file and append the following highlighted markup (roughly line 19):

<div class="navbar-collapse collapse ">
  <ul class="nav navbar-nav">
    <li class="{{home-selected}}">
      <a href="{{servlet-context}}/">Home</a></li>
    <li class="{{about-selected}}">
    <a href="{{servlet-context}}/about">About</a></li>
  </ul>
  <ul class="nav navbar-nav navbar-right">
    <li><a href="{{servlet-context}}/logout">Logout</a></li>
  </ul>
</div>

Save the file and refresh your browser, and you'll now see a Logout link in the top-right corner of your browser:

Creating the logout route

There's only one problem though: A link saying Logout doesn't make a lot of sense if you're not already authenticated. so change the code to the following:

<ul class="nav navbar-nav navbar-right">
  {% if is-authed? %}
    <li><a href="{{servlet-context}}/logout">Logout</a></li>
  {% else %}
    <li><a href="{{servlet-context}}/login">Login</a></li>
  {% endif %}
</ul>

We will now render the appropriate link if the is-authed? context value is true. But where is that value coming from? We need to set it on the context. Considering that this link will be on every single one of our pages, it makes sense for us to adjust the hipstr.layout/render function. This is the function we call every time we render an HTML template. We can associate the :is-authed? context value with the parameter map coming in:

  1. First, include the hipstr.models.user-model in hipstr.layout namespace:
    (:require …
             [hipstr.models.user-model :as user]
              …)
  2. Adjust the render function to associate the is-authed? key with the value returned by hipstr.models.user-model/is-authed?:
    (defn render [template & [params]]
      (let [params (-> (or params {})
                       (assoc :is-authed? (user/is-authed? nil)))]
      (RenderableTemplate. template params)))

The base.html template will now show the appropriate Login/Logout link, depending on the user's current authenticated status.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.18.101