A Simple CMS in Sinatra, Part III

This entry is part 3 of 3 in the series A Simple CMS in Sinatra

A Simple CMS in Sinatra

In part two of this tutorial series we finished putting together the basics of our Simple Content Management System. It’s now possible to create, edit, and delete pages with pretty URLs.

In this post, we’re going to look at adding an Administration section and defining which pages are public facing and which are private.

Logging In and Logging Out

First off, we’ll add some functionality to allow a user to log in and out. To do this, we need to configure our application to use sessions. Change the configure block near the top of main.rb to the following:

configure do
  Mongoid.load!("./mongoid.yml")
  enable :sessions
end

We can now access the session hash to keep track of whether a user is logged in from request to request.

Next, we will create a helper method called admin? that is true if the session[:admin] hash is true (this will be set as true when a user logs in and set to nil when the user logs out). Add the following line below the configure block in main.rb:

helpers do
  def admin?
    session[:admin]
  end
end

We now have a convenient way to check if a user is logged in or not in views and route handlers. Let’s use this to add a login button to our application.

We want this to be on every page, so change the layout.slim file in the views folder to the following:

doctype html
html
  head
    title= @title || "Simple Sinatra CMS"
    link rel="stylesheet" href="/styles/main.css"
  body
    - if admin?
      == slim :admin
    - else
      a.rounded.button href="/login" Log In
    h1.logo
      a href="/pages" Simple Sinatra CMS
    == yield

The section after the body now checks to see if the user is logged in (by checking if the admin? helper method that we just created is true). If they are, then we will display the admin partial view (we’ll create this in a minute).

If the user is not logged in, then a link is displayed to allow them to log in. This link has the classes .rounded.button that we created in the last tutorial so it appears as a button. It also contains the route /login, which we’ll handle shortly.

Let’s get the admin view sorted – save the following code as admin.slim in the views folder:

#admin
  nav
    ul
      li
        a.round.button href='/pages' Pages
      li
        a.round.button href='/pages/new' Add a page
      - if @page
        li
          a.round.button href="/pages/#{@page.id}/edit" Edit this page
        li
          a.round.button href="/pages/delete/#{@page.id}" Delete this page a.rounded.button href="/logout" Log Out

This adds the “create new page” button as well as buttons for edit and delete that only show if you are looking at an actual page. These buttons should only be available if the user is logged in. In the previous version of our application, they were placed in other views, so we need to remove them now and limit them to only being in the admin view.

In index.slim, remove the following line of code:

a.button.round href='/pages/new' Add a new page

Now in show.slim, remove the following code:

a.button href="/pages/#{@page.id}/edit" Edit 
a.button href="/pages/delete/#{@page.id}" Delete

To finish off, we need to create the actual route handlers that are used to log the user in and out. Add the following lines above the page route handlers in main.rb:

get('/login'){session[:admin]=true; redirect back}
get('/logout'){session[:admin]=nil; redirect back}

These route handlers set the session[:admin] hash to true when the user logs in and to nil when the user logs out by visiting the relevant URL. This information is retained in the session hash using cookies and is then used by the admin? helper method that we just created to check whether the user is logged in or out. Both route handlers also use the handy back helper method that Sinatra provides. This will take the user back to whichever page they were viewing before they logged in or out.

This should now all be working. Make sure you start the server running and then navigate to http://localhost:4567/pages and have a play around at logging in and out. You should only be able to see the buttons for adding, editing and deleting pages if you are logged in.

Screenshot5

The Most Insecure Security Ever?

The eagle-eyed amongst you might have noticed that this authentication system is not particularly strong – there isn’t even a password! This is because having a strong auth procedure is an important business and beyond the scope of this tutorial. What we have done here is put the pieces in place so that a suitable auth solution can then be baked in to the relevant places.

You might want take a look at the sinatra-auth gem (or one of the many other gems that provide auth). You could use Twitter Authentication or you could even roll your own solution.

For the purposes of this tutorial though, I just wanted to demonstrate how the application would function differently depending on whether the user is logged in or not.

Protecting Admin Features

We have a number of routes in our application that we don’t particularly want all users to access. In fact, most of the routes fall under this category, we actually only want the ‘show’ URLs to be visible to everybody.

We have successfully hidden the buttons that link to the admin routes, but that might not stop a determined or curious user from simply typing the URLs directly into the browser address bar. For example, you can just navigate to ‘http://localhost:4567/pages/new’ to add a new page.

We need to add some security to the routes themselves to actually only allow users to access these routes if they are logged in. This is done by creating a protected! helper method.

Add the following method inside the helpers block that we created earlier:

def protected!
  halt 401,"You are not authorized to see this page!" unless admin?
end

This uses Sinatra’s halt helper method to stop the request in its tracks and issue a 401 ‘unauthorized’ error with a custom message (this can be a string or you could create a view for it). Notice that we use the admin? helper method that we created at the beginning of this tutorial to check if the user is logged in, so the request will only be halted if the user is NOT logged in.

This method now needs to be added to all the route handlers that we want to protect. This means we need to change a lot of our route handlers to the following:

get '/pages/new' do
  protected!
  @page = Page.new
  slim :new
end

post '/pages' do
  protected!
  page = Page.create(params[:page])
  redirect to("/pages/#{page.id}")
end

put '/pages/:id' do
  protected!
  page = Page.find(params[:id])
  page.update_attributes(params[:page])
  redirect to("/pages/#{page.id}")
end

get '/pages/delete/:id' do
  protected!
  @page = Page.find(params[:id])
  slim :delete
end

delete '/pages/:id' do
  protected!
  Page.find(params[:id]).destroy
  redirect to('/pages')
end

get '/pages/:id/edit' do
  protected!
  @page = Page.find(params[:id])
  slim :edit
end

Now none of these routes will be accessible to any users that are not logged in, even if they type the route directly into the browser.

Different URLs for Different Users

In the last post, we created pretty URLs that are based on the title of the page. These are the URLs that we would like to use for the majority of users – they look better and we don’t want them to see the IDs of each page exposed in the URL.

When a user is logged in however, we want to use the URL that refers to the ID of the page. To do this we need to add a helper called url our helper block in main.rb:

def url_for page
  if admin?
    "/pages/" + page.id
  else
    "/" + page.permalink   
  end  
end

This basically uses an if ... else block to check if the user is logged in using our same admin? helper. If the user is logged in, then the URL will use the page’s ID, otherwise it will use the page’s pretty URL.

We can now just use this helper whenever we want to link to a page, confident that the correct URL will be displayed. Also, if we want to change the naming structure for the page URLs then we only have to do it in this one place.

The only place where we currently link to pages is in the page partial, so we need to change the page.slim view to the follwoing:

li
  a href="#{url_for page}" =page.title

Add Some Style

Just to finish off, we can make the list of buttons in the admin section appear as a horizontal list by adding the following piece of Sass code to the bottom of styles.scss:

#admin ul{
  list-style: none;
  margin: 0;
  padding: 0;
  li{
        display: inline-block;
  }
}

Now restart the server and play around with using the different functionality – check that the links are different depending on whether you are logged in or out.

Screenshot6

That’s All Folks!

In this post, we’ve added an admininstration section to our content management system, albeit with possibly the least secure authentication system around.

Despite the lack of security, the ability for users to log in has allowed us to separate the different views and functionality of the application. In the next post we’ll be looking at how to cache the pages as well as adding timestamps and versioning to the pages.

Please leave any comments below, as well as any requests for how you’d like to see this CMS develop.

A Simple CMS in Sinatra

<< A Simple CMS in Sinatra, Part II

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Anonymous

    Worth looking at Persona to handle the login/auth/security – you can add an extra layer of filtering to only allow certain email addresses/wildcards (e.g. email address ends with your company domain) so not just anyone can login. Makes life a lot easier, don’t have to worry about bcrypt/scrypt or password resets, and generally lets you spend time on the guts of your app!

    https://github.com/qzio/example-sinatra-persona

  • David

    Please note that if you are doing a modular Sinatra app you’ll net to enable :method_override to allow the magic comment “_method” in your forms.