Posts Tagged “spa”

Monday, November 29, 2021
  A Tour of myPrayerJournal v3: Bootstrap Integration

NOTE: This is the third post in a series; see the introduction for information on requirements and links to other posts in the series.

Many modern Single Page Application (SPA) frameworks include (or have plugins for) CSS transitions and effects. Combined with the speed of not having to do a full refresh, this is one of their best features. One might not think that a framework like htmx, which simply swaps out sections of the page, would have this; but if one were to think that, one would be wrong. Sadly, though, I did not utilize those aspects of htmx while I was migrating myPrayerJournal from v2 to v3; however, I will highlight the htmx way to do this in last section of this post.

myPrayerJournal v2 used a Vue plugin that provided Bootstrap v4 support; myPrayerJournal v3 uses Bootstrap v5. The main motivation I had to remain with Bootstrap was that I liked the actual appearance, and I know how it works. The majority of my “learning” on this project dealt with htmx; I did not want to add a UI redesign to the mix. Before we jump into the implementation, let me briefly explain the framework.

About Bootstrap

Bootstrap was originally called Twitter Bootstrap; it was the CSS framework that Twitter developed in their early iterations. It was, by far, the most popular framework at the time, and it was innovative in its grid layout system. Long before there was browser support for the styles that make layouts much easier to develop, and more responsive to differing screen sizes, Bootstrap's grid layout and size breakpoints made it easy to build a website that worked for desktop, tablet, or phone. Of course, there is a limit to what you can do with styling, so Bootstrap also has a JavaScript library that augments these styles, enabling the interactivity to which the modern web user is accustomed.

Version 5 of Bootstrap continues this tradition; however, it brings in even more utility classes, and supports Flex layouts as well. It is a mature library that continues to be maintained, and the project's philosophy seems to be “just enough” - it's not going to do everything for everyone, but in the majority of cases, it has exactly what the developer needs. It is not a bloated library that needs tree-shaking to avoid a ridiculous download size.

It is, by far, the largest payload in the initial page request:

  • Bootstrap - 48.6 kB (CSS is 24.8 kB; JavaScript is 23.8 kB, deferred until after render)
  • htmx - 11.8 kB
  • myPrayerJournal - 4.4 kB (CSS is 1.2 kB, JavaScript is 3.2 kB)

However, this gets the entire style and script, and allows us to use their layouts and interactive components. But, how do we get that interactivity from the server?

Hooking in to the htmx Request Pipeline

htmx provides several events to which an application can listen. In myPrayerJournal v3, I used htmx:afterOnLoad because I did not need the new content to be swapped in yet when the function fired. There are afterSwap and afterSettle events which will fire once those events have occurred, if you need to defer processing until those are complete.

There are two different Bootstrap script-driven components myPrayerJournal uses; let's take a look at toasts.

A Toast to Via htmx

Toasts are pop-up notifications that appear on the screen, usually for a short time, then fade out. In some cases, particularly if the toast is alerting the user to an error, it will stay on the screen until the user dismisses it, usually by clicking an “x” in the upper right-hand corner (even if the developer used a Mac!). Bootstrap provides a host of options for their toast component; for our uses, though, we will:

  • Place toasts in the bottom right-hand corner;
  • Allow multiple toasts to be visible at once;
  • Auto-hide success toasts; require others to be dismissed manually.

There are several different aspects that make this work.

The Toaster

Just like IRL toast comes out of a toaster, our toasts need a place from which to emerge. In the prior post, I mentioned that the footer does not get reloaded when a “page” request is made. There is also an element above the footer that also remains across these requests - defined here as the “toaster” (my term, not Bootstrap's).

/// Element used to display toasts
let toaster =
  div [ _ariaLive "polite"; _ariaAtomic "true"; _id "toastHost" ] [
    div [ _class "toast-container position-absolute p-3 bottom-0 end-0"; _id "toasts" ] []
    ]

This renders two empty divs with the appropriate style attributes; toasts placed in the #toasts div will display as we want them to.

Showing the Toast

Bootstrap provides data- attributes that can make toasts appear; however, since we are creating these in script, we need to use their JavaScript functions. The message coming from the server has the format TYPE|||The message. Let's look at the showToast function (the largest custom JavaScript function in the entire application):

const mpj = {
  // ...
  showToast (message) {
    const [level, msg] = message.split("|||")
  
    let header
    if (level !== "success") {
      const heading = typ => `<span class="me-auto"><strong>${typ.toUpperCase()}</strong></span>`
    
      header = document.createElement("div")
      header.className = "toast-header"
      header.innerHTML = heading(level === "warning" ? level : "error")
    
      const close = document.createElement("button")
      close.type = "button"
      close.className = "btn-close"
      close.setAttribute("data-bs-dismiss", "toast")
      close.setAttribute("aria-label", "Close")
      header.appendChild(close)
    }

    const body = document.createElement("div")
    body.className = "toast-body"
    body.innerText = msg
  
    const toastEl = document.createElement("div")
    toastEl.className = `toast bg-${level === "error" ? "danger" : level} text-white`
    toastEl.setAttribute("role", "alert")
    toastEl.setAttribute("aria-live", "assertlive")
    toastEl.setAttribute("aria-atomic", "true")
    toastEl.addEventListener("hidden.bs.toast", e => e.target.remove())
    if (header) toastEl.appendChild(header)
  
    toastEl.appendChild(body)
    document.getElementById("toasts").appendChild(toastEl)
    new bootstrap.Toast(toastEl, { autohide: level === "success" }).show()
  },
  // ...
}

Here's what's going on in the code above:

  • Line 4 splits the level from the message
  • Lines 6-20 (let header) create a header and close button if the message is not a success
  • Lines 22-24 (const body) create the body div with attributes Bootstrap's styling expects
  • Lines 26-30 (const toastEl) create the div that will contain the toast
  • Line 31 adds an event handler to remove the element from the DOM once the toast is hidden
  • Lines 32 and 34 add the optional header and mandatory body to the toast div
  • Line 35 adds the toast to the page (within the toasts inner div defined above)
  • Line 36 initializes the Bootstrap JavaScript component, auto-hiding on success, and shows the toast

(If you've never used JavaScript to create elements that are added to an HTML document, this probably looks weird and verbose; if you have, you look at it and think "well, they're not wrong…")

So, we have our toaster, we know how to put bread notifications in it - but how do we get the notifications from the server?

Receiving the Toast

The code to handle this is part of the htmx:afterOnLoad handler:

htmx.on("htmx:afterOnLoad", function (evt) {
  const hdrs = evt.detail.xhr.getAllResponseHeaders()
  // Show a message if there was one in the response
  if (hdrs.indexOf("x-toast") >= 0) {
    mpj.showToast(evt.detail.xhr.getResponseHeader("x-toast"))
  }
  // ...
})

This looks for a custom HTTP header of X-Toast (all headers are lowercase from that xhr call), and if it's found, we pass the value of that header to the function above. This check occurs after every htmx network request, so there is nothing special to configure; “page” requests are not the only requests capable of returning a toast notification.

There is one more part; how does the toast get to the browser?

Sending the Toast

The last paragraph gave it away; we set a header on the response. This seems straightforward, and is in most cases; but once again, POST-Redirect-GET (P-R-G) complicates things. Here are the final two lines of the successful path of the request update handler:

Messages.pushSuccess ctx "Prayer request updated successfully" nextUrl
return! seeOther nextUrl next ctx

If we set a message in the response header, then redirect (remember that XMLHttpRequest handles redirects silently), the header gets lost in the redirect. Here, Messages.pushSuccess places the success message (and return URL) in a dictionary, indexed by the user's ID. Within the function that renders every result (partial, “page”-like, or full results), this dictionary is checked for a message and URL, and if one exists, it includes it. (If it is returned to the function below, it has already been removed from the dictionary.)

/// Send a partial result if this is not a full page load (does not append no-cache headers)
let partialStatic (pageTitle : string) content : HttpHandler =
  fun next ctx -> backgroundTask {
    let  isPartial = ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh
    let! pageCtx   = pageContext ctx pageTitle content
    let  view      = (match isPartial with true -> partial | false -> view) pageCtx
    return! 
      (next, ctx)
      ||> match user ctx with
          | Some u ->
              match Messages.pop u with
              | Some (msg, url) -> setHttpHeader "X-Toast" msg >=> withHxPush url >=> writeView view
              | None -> writeView view
          | None -> writeView view
    }

A quick overview of this function:

  • Line 4 determines if this an htmx boosted request (a “page”-like requests)
  • Line 5 creates a rendering context for the page
  • Line 6 renders the view to a string, calling partial or view with the page rendering context
  • Lines 10-13 are only executed if a user is logged on, and line 12 is the one that appends a message and a new URL

A quick note about line 12: the >=> operator joins Giraffe HttpHandlers together. An HttpHandler takes an HttpContext and the next function to be executed, and returns a Task<HttpContext option> (an asynchronous call that may or may not return a context). If there is no context returned, the chain stops; the function can also return an altered context. It is good practice for an HttpHandler to make a single change to the context; this keeps them simple, and allows them to be plugged in however the developer desires. Thus, the setHttpHeader call adds the X-Toast header, the withHxPush call adds the HX-Push header, and the writeView call sets the response body to the rendered view.

The new URL part does not actually make the browser do anything; it simply pushes the given URL onto the browser's history stack. Technically, the browser receives the content from the P-R-G as the response to its POST; as we're replacing the current page, though, we need to make sure the URL stays in sync.

Of note is that not all toasts are this complex. For example, the “cancel snooze” handler return looks like this:

return! (withSuccessMessage "Request unsnoozed" >=> Components.requestItem requestId) next ctx

...while the withSuccessMessage handler is:

/// Add a success message header to the response
let withSuccessMessage : string -> HttpHandler =
  sprintf "success|||%s" >> setHttpHeader "X-Toast"

No dictionary, no redirect, just a single response that will show a toast.

You made it - the toast section is toast! There is one more interesting interaction, though; that of the modal dialog.

Bootstrap's implementation of modal dialogs also uses JavaScript; however, for the purposes of the modals used in myPrayerJournal v3, we can use the data- attributes to show them. Here is the view for a modal dialog that allows the user to snooze a request (hiding it from the active list until the specified date); this is rendered a single time on the journal view page:

div [
  _id             "snoozeModal"
  _class          "modal fade"
  _tabindex       "-1"
  _ariaLabelledBy "snoozeModalLabel"
  _ariaHidden     "true"
  ] [
  div [ _class "modal-dialog modal-sm" ] [
    div [ _class "modal-content" ] [
      div [ _class "modal-header" ] [
        h5 [ _class "modal-title"; _id "snoozeModalLabel" ] [ str "Snooze Prayer Request" ]
        button [ _type "button"; _class "btn-close"; _data "bs-dismiss" "modal"; _ariaLabel "Close" ] []
        ]
      div [ _class "modal-body"; _id "snoozeBody" ] [ ]
      div [ _class "modal-footer" ] [
        button [ _type "button"; _id "snoozeDismiss"; _class "btn btn-secondary"; _data "bs-dismiss" "modal" ] [
          str "Close"
          ]
        ]
      ]
    ]
  ]

Notice that #snoozeBody is empty; we fill that when the user clicks the snooze icon:

button [
  _type     "button"
  _class    "btn btn-secondary"
  _title    "Snooze Request"
  _data     "bs-toggle" "modal"
  _data     "bs-target" "#snoozeModal"
  _hxGet    $"/components/request/{reqId}/snooze"
  _hxTarget "#snoozeBody"
  _hxSwap   HxSwap.InnerHtml
  ] [ icon "schedule" ]

This uses data-bs-toggle and data-bs-target, Bootstrap attributes, to show the modal. It also uses hx-get to load the snooze form for that particular request, with hx-target targeting the #snoozeBody div from the modal definition. Here is how that form is defined:

/// The snooze edit form
let snooze requestId =
  let today = System.DateTime.Today.ToString "yyyy-MM-dd"
  form [
    _hxPatch  $"/request/{RequestId.toString requestId}/snooze"
    _hxTarget "#journalItems"
    _hxSwap   HxSwap.OuterHtml
    ] [
    div [ _class "form-floating pb-3" ] [
      input [ _type "date"; _id "until"; _name "until"; _class "form-control"; _min today; _required ]
      label [ _for "until" ] [ str "Until" ]
      ]
    p [ _class "text-end mb-0" ] [ button [ _type "submit"; _class "btn btn-primary" ] [ str "Snooze" ] ]
    ]

Here, the form uses hx-patch to submit the data to the snooze endpoint. The target for the response, though, is #journalItems; this is the element that holds all of the prayer request cards. Snoozing a request will remove it from the active list, so the list needs to be refreshed; this will make that happen.

Look back at the modal definition; at the bottom, there is a “Close” button. We will use this to dismiss the modal once the update succeeds. In the Giraffe handler to snooze a request, here is its return statement:

return!
  (withSuccessMessage $"Request snoozed until {until.until}"
  >=> hideModal "snooze"
  >=> Components.journalItems) next ctx

Notice that hideModal handler?

/// Hide a modal window when the response is sent
let hideModal (name : string) : HttpHandler =
  setHttpHeader "X-Hide-Modal" name

Yes, it's another HTTP header! One can certainly get carried away with custom HTTP headers, but their very existence is to communicate with the client (browser) outside of the visible content of the page. Here, we're passing the name “snooze” to this header; in our htmx:afterOnLoad handler, we'll consume this header:

htmx.on("htmx:afterOnLoad", function (evt) {
  const hdrs = evt.detail.xhr.getAllResponseHeaders()
  // ...
  // Hide a modal window if requested
  if (hdrs.indexOf("x-hide-modal") >= 0) {
    document.getElementById(evt.detail.xhr.getResponseHeader("x-hide-modal") + "Dismiss").click()
  }
})

The “Close” button on our modal was given the id of snoozeDismiss; this mimics the user clicking the button, which Bootstrap's data- attributes handle from there. Of all the design choices and implementations I did in this conversion, this part strikes me as the most "hack"y. However, I did try to hook into the Bootstrap modal itself, and hide it via script; however, it didn't like initializing a modal a second time, and I could not get a reference to it from the htmx:afterOnLoad handler. Clicking the button works, though, even when it's done from script.

CSS Transitions in htmx

This post has already gotten much longer than I had planned, but I wanted to make sure I covered this.

  • When htmx requests are in flight, the framework makes it easy to show indicators.
  • I mentioned swapping and settling when discussing the events htmx exposes. The way this is done, CSS transitions will render as expected. They have a host of examples to spark your imagination.

As I was keeping the UI the same, I did not end up using these options; however, their presence demonstrates that htmx is a true batteries-included SPA framework.


Up next, we'll step away from the front end and dig into LiteDB.

Categorized under , , ,
Tagged , , , , , , , , , , , , , ,

Sunday, November 28, 2021
  A Tour of myPrayerJournal v3: The User Interface

NOTE: This is the second post in a series; see the introduction for information on requirements and links to other posts in the series.

If you are a seasoned Single Page Application (SPA) framework developer, you likely think about interactivity in a particular way. Initially, I focused on replacing each interactive piece in isolation. In the end, though, requests for “pages” returned almost everything but the HTML head info and the displayed footer - and I was happy about it. Keep that in mind as I walk you down the path I have already traveled; keep an open mind, and read to the end before forming strong opinions either way.

The $.05 Tour of Pug and Giraffe View Engine

Understanding the syntax of both Pug and Giraffe View Engine will help you if you click any of the source code example links. While a complete explanation of these two templating languages would make this long post much longer than it already is, here are some short examples of their syntax. Using a string variable who with the contents “World”, we will show both languages rendering:

<p id="example" class="greeting">Hello <strong>World</strong></p>

myPrayerJournal v2 used Pug templates in Vue to render the user interface. Pug uses indentation-sensitive tag/content pairs (or blocks), with JavaScript syntax for attributes, to generate HTML. To generate the example paragraph, the shortest template would look like:

p.greeting(id="example") Hello #[strong= who]

myPrayerJournal v3 uses Giraffe View Engine, which uses F# lists to generate HTML from a very HTML-looking domain-specific language (DSL). The example paragraph would be generated with:

p [ _id "example"; _class "greeting" ] [ str "Hello "; strong [] [ who ] ]

Given those examples, let's dig into the conversion.

The Menu

The menu across the top of the application was one of the first items I needed to convert. The menu needs to be different, depending on whether there is a user logged on or not. Also, if a user is logged on, the menu can still be different; the “Snoozed” menu item only appears if the user has any snoozed requests. The application uses Auth0 to manage users (which is how it is open to Microsoft and Google accounts), and I wanted to preserve this; my requests are tied to the ID provided by Auth0, so that did not need to change.

In the Vue version, the system used Auth0's SPA library that exposed whether there was a user logged on or not. Also, once a user was logged on, the API sent all the user's active requests, which included snoozed requests; once this API call returned, the application can turn on the “Snoozed” menu item. In the htmx version, though, this information is all generated on the server. My initial process was to use an hx-get to get the menu HTML snippet, using an hx-trigger of load to fill in this spot of the page when the page was loaded. I also (initially) implemented a custom HTML header to include in responses, and if that header was found, I would trigger a refresh on the menu; the eventual solution included the navbar in “page” refreshes.

(See the Vue “Navigation” component that became the Giraffe View Engine “navbar” function)

“New Page” in htmx

This leads directly into a discussion of how myPrayerJournal is still considered a SPA. In the Vue version, “pages” were Vue Single-File Components (SFCs) under the /components directory. (In the years since myPrayerJournal v1, the default Vue template has changed to place these SFCs under /views, while /components is reserved for shared components.) These view components rendered into a custom component within the main tag (using Vue router's router-view tag), while the nav component was reactive, based on the user logging on/off and snoozing requests.

In myPrayerJournal v3, “page” views target the #top section element. If the request is for a full page load, the HTML head content is rendered, as is the body's footer content; none of these change until a new version of the application is released. If the request is an htmx request, though, the only thing rendered is a new #top section, which includes the navigation bar and the page content. While this does approach a full “page load”, there are some key differences:

  • The page contents are refreshed based on one HTTP request (no extra request or processing required for the navbar);
  • The HTML head content is responsible for most of the large HTTP requests, such as those for JavaScript libraries (and is excluded from non-full-page views);
  • The page footer is not included.

Note the difference between the full view layout and the partial layout. Also, within the application's request handlers, there is a partial return function that determines whether this is an htmx-initiated page view request (in which case a partial view is returned) or a full page request (which returns the entire template).

Updating the Page Title

One of the most unexpectedly-vexing parts of a SPA is determining how the browser's title bar will be updated when navigation occurs. (I understand why it's challenging; what I do not understand is why it took major frameworks so long to devise a built-in way of handling this.) Coming from that world, I had originally implemented yet another custom header, pushing the title from the server, and used a request listener to update the title if the header was present. As I dug in further, though, I learned that htmx will update the document title any time a request payload has an HTML title element in its head. If you look at both layouts in the preceding paragraph, you'll notice that they include a head element with a title tag. This is how easy it should be, and with htmx, this is how easy it is.

At this point, there is a pattern emerging. The thought process behind an htmx-powered website is much different than a JavaScript-based SPA framework; and, in the majority of cases, it has been less complex. Now, let me contradict what I just said.

POST-Redirect-GET

In myPrayerJournal v2, updating a prayer request followed this flow:

  • Display the edit page, with the request details filled in
  • When the user saved the request, return an empty 200 OK response
  • Using Vue, display a notification, refresh the journal, then re-render the page where the user had been when they clicked “Edit” (there are multiple places from which requests can be edited)

While there are no redirects here, this is the classic traditional-web-application scenario where the “POST-Redirect-GET” (P-R-G) pattern is used. By using this pattern, the “Back” button on the browser still works. If you try to go back to the result of a POST request, the browser will warn you that your action will result in the data being resubmitted (not usually what you want to do). By redirecting, though, the result of a POST becomes a GET, which does not change any data. For traditional web applications, this is the user-friendliest way to handle updates.

In the htmx examples, they show an example of inline editing. This led to my first plan - change the request edit “page” to be a component, where the HTML for the displayed list was replaced by the form, and then the “Save” action returns the new HTML. This requires no P-R-G, as these actions have no effect on the “Back” button. It worked fine, but there were some things that weren't quite right:

  • New requests needed their own page; I was going to have to duplicate the edit form for the “new” page, and introduce some complexity in determining how to render the results.
  • Some updates required refreshing the list of requests, not just replacing the text and action buttons.

At this point, I was also starting to realize “if you think something is hard to do in htmx, you probably aren't trying to do it correctly.” So, I decided to try to replicate the “edit page” flow of v2 in v3. Creating the page was easy enough, and I was able to use the returnTo parameter in the function to both provide a “Cancel” button and redirect the user to the right place after saving. Easy, right? Well… Not quite.

htmx uses XMLHttpRequest (XHR) to send its requests, which has some interesting behavior; it follows redirects! When I submitted my form, it received the request (with htmx's HX-Request header set), and the server returned the redirect. XHR saw this, and followed it; however, it used the same method. (It was POSTing to the new URL.) The fix for this, though, was not easy to find, but easy to implement; use HTTP response code 303 (see other) instead of 307 (moved temporarily). Using this, combined with using hx-target="#top" on the form, allowed the P-R-G pattern to work successfully without double-POSTing and without a full-page refresh.

htmx Support in Giraffe and Giraffe View Engine

As I developed this, I was also building up extensions for Giraffe to handle the htmx request and response headers, as well as the attributes needed to generate htmx-aware markup from Giraffe View Engine. This project, called Giraffe.Htmx, is now published on NuGet. There are two packages; Giraffe.Htmx provides server-side support for the request and response headers, and Giraffe.ViewEngine.Htmx provides support for generating htmx attributes. I wrote about it when it was released, so I won't rehash the entire thing here.

Final UI Thoughts

htmx is much less complex than any other front-end JavaScript SPA framework I have ever used - which, for context, includes Angular, Vue, React, Ember, Aurelia, and Elm. Both in development and in production use, I cannot tell that the payloads are slightly larger; navigation is fast and smooth. Though I have yet to change anything since going live with myPrayerJournal v3, I know that maintenance will be quite straightforward (to be further explored in the conclusion post).

The UI for myPrayerJournal uses Bootstrap, a UI framework which has its own script, and htmx plays quite nicely with it. The next post in this series will describe how I interact with both Bootstrap and htmx, using modals and toasts on this “traditional” web application.

Categorized under , , , ,
Tagged , , , , , , , , , , , , , , , , , ,

Thursday, August 30, 2018
  A Tour of myPrayerJournal: Authentication

NOTES:

  • This is post 5 in a series; see the introduction for all of them, and the requirements for which this software was built.
  • Links that start with the text “mpj:” are links to the 1.0.0 tag (1.0 release) of myPrayerJournal, unless otherwise noted.

At this point in our tour, we're going to shift to a cross-cutting concern for both app and API - authentication. While authentication and authorization are distinct concerns, the authorization check in myPrayerJournal is simply “Are you authenticated?” So, while we'll touch on authorization, and it will seem like a synonym for authentication, remember that they would not be so in a more complex application.

Deciding on Auth0

Auth0 provides authentication services; they focus on one thing, and getting that one thing right. They support simple username/password authentication, as well as integrations with many other providers. As “minimalist” was one of our goals, not having to build yet another user system was appealing. As an open source project, Auth0 provides these services at no cost. They're the organization behind the JSON Web Token (JWT) standard, which allows base-64-encoded, encrypted JSON to be passed around as proof of identity.

This decision has proved to be a good one. In the introduction, we mentioned all of the different frameworks and server technologies we had used before settling on the one we did. In all but one of these “roads not further traveled”1, authentication worked. They have several options for how to use their service; you can bring in their library and host it yourself, you can write your own and make your own calls to their endpoints, or you can use their hosted version. We opted for the latter.

Integrating Auth0 in the App

JavaScript seems to be Auth0's primary language. They provide an npm package to support using the responses that will be returned from their hosted login page. The basic flow is:

  • The user clicks a link that executes Auth0's authorize() function
  • The user completes authorization through Auth0
  • Auth0 returns the result and JWT to a predefined endpoint in the app
  • The app uses Auth0's parseHash() function to extract the JWT from the URL (a GET request)
  • If everything is good, establish the user's session and proceed

myPrayerJournal's implementation is contained in AuthService.js (mpj:AuthService.js). There is a file that is not part of the source code repository; this is the file that contains the configuration variables for the Auth0 instance. Using these variables, we configure the WebAuth instance from the Auth0 package; this instance becomes the execution point for our other authentication calls.

Using JWTs in the App

We'll start easy. The login() function simply exposes Auth0's authorize() function, which directs the user to the hosted log on page.

The next in logical sequence, handleAuthentication(), is called by LogOn.vue (mpj:LogOn.vue) on line 16, passing in our store and the router. (In our last post, we discussed how server requests to a URL handled by the app should simply return the app, so that it can process the request; this is one of those cases.) handleAuthentication() does several things:

  • It calls parseHash() to extract the JWT from the request's query string.
  • If we got both an access token and an ID token:
    • It calls setSession(), which saves these to local storage, and schedules renewal (which we'll discuss more in a bit).
    • It then calls Auth0's userInfo() function to retrieve the user profile for the token we just received.
    • When that comes back, it calls the store's (mpj:store/index.js) USER_LOGGED_ON mutation, passing the user profile; the mutation saves the profile to the store, local storage, and sets the Bearer token on the API service (more on that below as well).
    • Finally, it replaces the current location (/user/log-on?[lots-of-base64-stuff]) with the URL /journal; this navigates the user to their journal.
  • If something didn't go right, we log to the console and pop up an alert. There may be a more elegant way to handle this, but in testing, the only way to reliably make this pop up was to mess with things behind the scenes. (And, if people do that, they're not entitled to nice error messages.)

Let's dive into the store's USER_LOGGED_ON mutation a bit more; it starts on line 68. The local storage item and the state mutations are pretty straightforward, but what about that api.setBearer() call? The API service (mpj:api/index.js) handles all the API calls through the Axios library. Axios supports defining default headers that should be sent with every request, and we'll use the HTTP Authorization: Bearer [base64-jwt] header to tell the API what user is logged in. Line 18 sets the default authorization header to use for all future requests. (Back in the store, note that the USER_LOGGED_OFF mutation (just above this) does the opposite; it clears the authorization header. The logout() function in AuthService.js clears the local storage.)

At this point, once the user is logged in, the Bearer token is sent with every API call. None of the components, nor the store or its actions, need to do anything differently; it just works.

Maintaining Authentication

JWTs have short expirations, usually expressed in hours. Having a user's authentication go stale is not good! The scheduleRenewal() function in AuthService.js schedules a behind-the-scenes renewal of the JWT. When the time for renewal arrives, renewToken() is called, and if the renewal is successful, it runs the result through setSession(), just as we did above, which schedules the next renewal as its last step.

For this to work, we had to add /static/silent.html as an authorized callback for Auth0. This is an HTML page that sits outside of the Vue app; however, the usePostMessage: true parameter tells the renewal call that it will receive its result from a postMessage call. silent.html uses the Auth0 library to parse the hash and post the result to the parent window.2

Using JWTs in the API

Now that we're sending a Bearer token to the API, the API can tell if a user is logged in. We looked at some of the handlers that help us do that when we looked at the API in depth. Let's return to those and see how that is.

Before we look at the handlers, though, we need to look at the configuration, contained in Program.fs (mpj:Program.fs). You may remember that Giraffe sits atop ASP.NET Core; we can utilize its JwtBearer methods to set everything up. Lines 38-48 are the interesting ones for us; we use the UseAuthentication extension method to set up JWT handling, then use the AddJwtBearer extension method to configure our specific JWT values. (As with the app, these are part of a file that is not in the repository.) The end result of this configuration is that, if there is a Bearer token that is a valid JWT, the User property of the HttpContext has an instance of the ClaimsPrincipal type, and the various properties from the JWT's payload are registered as Claims on that user.

Now we can turn our attention to the handlers (mpj:Handlers.fs). authorize, on line 72, calls user ctx, which is defined on lines 50-51. All this does is look for a claim of the type ClaimTypes.NameIdentifier. This can be non-intuitive, as the source for this is the sub property from the JWT3. A valid JWT with a sub claim is how we tell we have a logged on user; an authenticated user is considered authorized.

You may have noticed that, when we were describing the entities for the API, we did not mention a User type. The reason for that is simple; the only user information it stores is the sub. Requests are assigned by user ID, and the user ID is included with every attempt to make any change to a request. This eliminates URL hacking or rogue API posting being able to get anything meaningful from the API.

The userId function, just below the user function, extracts this claim and returns its value, and it's used throughout the remainder of Handlers.fs. add (line 160) uses it to set the user ID for a new request. addHistory (line 192) and addNote (line 218) both use the user ID, as well as the passed request ID, to try to retrieve the request before adding history or notes to it. journal (line 137) uses it to retrieve the journal by user ID.

 

We now have a complete application, with the same user session providing access to the Vue app and tying all API calls to that user. We also use it to maintain data security among users, while truly outsourcing all user data to Microsoft or Google (the two external providers currently registered). We do still have a few more stops on our tour, though; the next is the back end data store.


1 Sorry, Elm; it's not you, it's me…

2 This does work, but not indefinitely; if I leave the same browser window open from the previous day, I still have to sign in again. I very well could be “doing it wrong;” this is an area where I probably experienced the most learning through creating this project.

3 I won't share how long it took me to figure out that sub mapped to that; let's just categorize it as “too long.” In my testing, it's the only claim that doesn't come across by its JWT name.

Categorized under , , ,
Tagged , , , , , , , ,

Wednesday, August 29, 2018
  A Tour of myPrayerJournal: The API

NOTES:

  • This is post 4 in a series; see the introduction for all of them, and the requirements for which this software was built.
  • Links that start with the text “mpj:” are links to the 1.0.0 tag (1.0 release) of myPrayerJournal, unless otherwise noted.

Now that we have a wonderful, shiny, reactive front end, we need to be able to get some data into it. We'll be communicating via JSON between the app and the server. In this post, we'll also attempt to explain some about the F# language features used as part of the API.

The Data

The entities are defined in Data.fs (mpj:Data.fs). We'll dig into them more fully in the “data store” post, but for now, we'll just focus on the types and fields. We have four types: History (lines 33-39), Note (lines 67-71), Request (lines 94-110), and JournalRequest (lines 153-173). A Request can have multiple Notes and History entries, and JournalRequest is based on a view that pulls these together and computes things like the current text of the request and when it should be displayed again.

We apply no special JSON transformations, so the fields in these record types are the properties in the exported JSON.

The URLs

To set the API apart from the rest of the URLs, they all start with /api/. Request URLs generally follow the form request/[id]/[action], and there is a separate URL for the journal. Line 54 in Program.fs (mpj:Program.fs) has the definition of the routes. We used Giraffe's Token Router instead of the traditional one, as we didn't need to support any URL schemes it doesn't. The result really looks like a nice, clean “table of contents” for the routes support by the API.1

We aren't done with routes just yet, though. Let's take a look at that notFound handler (mpj:Handlers.fs); it's on line 27. Since we're serving a SPA, we need to return index.html, the entry point of the SPA, for URLs that belong to it. Picture a user sitting at https://prayerjournal.me/journal and pressing “Refresh;” we don't want to return a 404! Since the app has a finite set of URL prefixes, we'll check to see if one of those is the URL. If it is, we send the Vue app; if not, we send a 404 response. This way, we can return true 404 responses for the inevitable hacking attempts we'll receive (pro tip, hackers - /wp-admin/wp-upload.php does not exist).

Defining the Handlers

Giraffe uses the term “handler” to define a function that handles a request. Handlers have the signature HttpFunc -> HttpContext -> Task<HttpContext option> (aliased as HttpHandler), and can be composed via the >=> (“fish”) operator. The option part in the signature is the key in composing handler functions. The >=> operator creates a pipeline that sends the output of one function into the input of another; however, if a function fails to return a Some option for the HttpContext parameter, it short-circuits the remaining logic.2

The biggest use of that composition in myPrayerJournal is determining if a user is logged in or not. Authorization is also getting its own post, so we'll just focus on the yes/no answer here. The authorized handler (line 71) looks for the presence of a user. If it's there, it returns next ctx, where next is the next HttpFunc and ctx is the HttpContext it received; this results in a Task<HttpContext option> which continues to process, hopefully following the happy path and eventually returning Some. If the user is not there, though, it returns the notAuthorized handler, also passing next and ctx; however, if we look up to line 67 and the definition of the notAuthorized handler, we see that it ignores both next and ctx, and returns None. However, notice that this handler has some fish composition in it; setStatusCode returns Some (it has succeeded) but we short-circuit the pipeline immediately thereafter.

We can see this in use in the handler for the /api/journal endpoint, starting on line 137. Both authorize and the inline function below it have the HttpHandler signature, so we can compose them with the >=> operator. If a user is signed in, they get a journal; if not, they get a 403.

When a handler is expecting a parameter from a route, the handler's signature is a bit different. The handler for /api/request/[id], on line 246, has an extra parameter, reqId. If we look back in Program.fs line 64, we see where this handler is assigned its route; and, if you compare it to the route for /api/journal on line 59, you'll see that it looks the same. The difference here is the expectations of route (for the journal) vs. routef (for the request). route expects no parameter, but routef will extract the parameters, Printf style, and pass them as parameters that precede the HttpHandler signature.

Executing the Handlers

myPrayerJournal has GET, POST, and PATCH routes defined. We'll look at representative examples of each of these over the next few paragraphs.

For our GET example, let's return to the Request.get handler, starting on line 246. Once we clear the authorize hurdle, the code attempts to retrieve a JournalRequest by the request ID passed to the handler and the user ID passed as part of the request. If it exists, we return the JSON representation of it; if not, we return a 404. Note the order of parameters to the json result handler - it's json [object] next ctx (or json [object] HttpHandler). We also defined an asJson function on line 75; this flips the parameters so that the output object is the last parameter (asJson next ctx [object]), enabling the flow seen in the journal handler on line 137.

For our POST example, we'll use Request.addNote, starting on line 218. It checks authorization, and to make sure the request exists for the current user. If everything is as it should be, it then uses Giraffe's BindJsonAsync<'T> extension method to create an instance of the expected input form. Since the handler doesn't have a place to specify the expected input object, this type of model binding cannot happen automatically; the upside is, you don't waste CPU cycles trying to do it if you don't need it. Once we have our model bound, we create a new Note, add it, then return a 201 response.

PATCH handlers are very similar; Request.snooze is one such handler, starting on line 291. The flow is very similar as the one for Request.addNote, except that we're updating instead of adding, and we return 204 instead of 201.

Configuring the Web Server

Giraffe enables Suave-like function composition on top of Kestrel and the ASP.NET Core infrastructure. Rather than using the Startup class, myPrayerJournal uses a functional configuration strategy. The calls are in Program.fs starting on line 115; there are lots of other guides on how to configure ASP.NET Core, so I won't say too much more about it.

Notice, though, line 31. Earlier, we discussed the >=> operator and how it worked. This is an example of the >> composition operator, which is part of F#. For functions whose output can be run directly into the next function's input, the >> operator allows those functions to be composed into a single function. If we were to look at the signature of the composed function within the parentheses, its signature would be string -> unit; so, we pass the string “Kestrel” to it, and it runs through and returns unit, which is what we expect for a configuration function. Here's how that breaks down:

  • ctx.Configuration.GetSection's signature is string -> IConfigurationSection
  • opts.Configure's signature is IConfiguration -> KestrelConfigurationLoader (IConfigurationSection implements IConfiguration)
  • ignore's signature is 'a -> unit

If this still doesn't make sense, perhaps this will help. The Configure.kestrel function could also have been written using the |> piping operator instead, with the equivalent code looking like:

  let kestrel (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) =
    ctx.Configuration.GetSection "Kestrel"
    |> opts.Configure
    |> ignore

 

That concludes our tour of the API for now, though we'll be looking at it again next time, when we take a deeper dive into authentication and authorization using Auth0.


1 While we tried to follow REST principles in large part, the REST purists would probably say that it's not quite RESTful enough to claim the name. But, hey, we do use PATCH, so maybe we'll get partial credit…

2 Scott Wlaschin has a great post entitled “Railway Oriented Programming” that explains this concept in general, and the fish operator specifically. Translating his definition to Giraffe's handlers, the first function is switch1, the next parameter is switch2, and the HttpContext is the x parameter; instead of Success and Failure, the return type utilizes the either/or nature of an option being Some or None. If you want to understand what makes F# such a great programming model, you'll spend more time on his site than on The Bit Badger Blog.

Categorized under , ,
Tagged , , , , , , , , , , , , , , , , , , ,