Roda framework logo image

In my last article we built a simple Roda website, which let us deliver some rather static pages. We’ll build on that work by implementing some basic blogging features such as User accounts and Posts.

Note: as this article was posted in April 2015 I can not guarantee it will still work with the latest versions Roda.

We’ll be using a PostgreSQL database, the defacto DB of many Rubyist, which we’ll interact with using Sequel, an excellent ORM by Jeremy Evans, who is also the developer behind Roda.

Please make sure to install PostgreSQL before continuing. Once that’s done we need to add the appropriate RubyGems to our Gemfile;

# ./Gemfile
gem "pg", "~> 0.18.1"
gem "sequel", "~> 4.21.0"

After running bundle install we need to connect Sequel to the database. In a real world application we’d want to keep our myapp.rb uncluttered by placing the configuration in a separate database.yml file, and ORM initialization in database.rb, but I’ll simplify that for this tutorial by including everything right in the main app file.

# ./myapp.rb
require "roda"
require "sequel"

database = "myapp_development"
user     = ENV["PGUSER"]
password = ENV["PGPASSWORD"]
DB = Sequel.connect(adapter: "postgres", database: database, host: "127.0.0.1", user: user, password: password)

class Myapp < Roda
  # ...
end

It can be useful to use environment variables for things like usernames and passwords, which is what I’ve done here, but if you’d like, just replace the ENV directly with your database user/password credentials. If your host setting is different then make sure to change that too.

Rake Tasks for the Database

Using the Psql interactive terminal to create your database and tables can get tiresome pretty quickly, so we’ll create some useful rake tasks (actually based on the Padrino generators!) to make our life easier. These tasks will allow us to create/drop the database using just the db:create and db:drop rake commands, along with managing the tables with a db:migrate command.

This is pretty typical rake code so I won’t go into too much explanation, but please do read the Rake documentation if you’re not sure what is going on here.

$ mkdir -p ~/myapp/lib/tasks

We’ll need a Rakefile with some basic set up configuration;

# ./Rakefile

#!/usr/bin/env rake
task :app do
  require "./myapp"
end
Dir[File.dirname(__FILE__) + "/lib/tasks/*.rb"].sort.each do |path|
  require path
end

And now for the tasks themselves;

# ./lib/tasks/database.rb
namespace :db do
  namespace :migrate do
    desc "Perform migration up to latest migration available"
    task :up => :app do
      Sequel.extension(:migration)
      Sequel::Migrator.run(Sequel::Model.db, "db/migrate")
      puts "<= db:migrate:up executed"
    end
    desc "Perform migration down (erase all data)"
    task :down => :app do
      Sequel.extension(:migration)
      Sequel::Migrator.run(Sequel::Model.db, "db/migrate", target: 0)
      puts "<= db:migrate:down executed"
    end
  end

  desc "Perform migration up to latest migration available"
  task :migrate => "db:migrate:up"

  desc "Create the database"
  task :create => :app do
    config = Sequel::Model.db.opts
    config[:charset] = "utf8" unless config[:charset]
    puts "=> Creating database '#{config[:database]}'"
    create_db(config)
    puts "<= db:create executed"
  end
  desc "Drop the database"
  task :drop => :app do
    Sequel::Model.db.disconnect
    config = Sequel::Model.db.opts
    puts "=> Dropping database '#{config[:database]}'"
    drop_db(config)
    puts "<= db:drop executed"
  end
end

def self.create_db(config)
  environment = {}
  environment["PGUSER"]     = config[:user]
  environment["PGPASSWORD"] = config[:password]
  arguments = []
  arguments << "--encoding=#{config[:charset]}" if config[:charset]
  arguments << "--host=#{config[:host]}" if config[:host]
  arguments << "--username=#{config[:user]}" if config[:user]
  arguments << config[:database]
  Process.wait Process.spawn(environment, "createdb", *arguments)
end
def self.drop_db(config)
  environment = {}
  environment["PGUSER"]     = config[:user]
  environment["PGPASSWORD"] = config[:password]
  arguments = []
  arguments << "--host=#{config[:host]}" if config[:host]
  arguments << "--username=#{config[:user]}" if config[:user]
  arguments << config[:database]
  Process.wait Process.spawn(environment, "dropdb", *arguments)
end

The create_db and drop_db methods do nothing more than generate and execute postgres commands. We pass in the Sequel configuration details (config = Sequel::Model.db.opts) set in myapp.rb, which are used to set the credentials for the postgres createdb and dropdb commands.

If you’ve previously worked with Rails then you’ll be familiar with the command for listing available tasks;

$ rake -T

which should display the following list;

rake db:create        # Create the database
rake db:drop          # Drop the database
rake db:migrate       # Perform migration up to latest migration available
rake db:migrate:down  # Perform migration down (erase all data)
rake db:migrate:up    # Perform migration up to latest migration available

With these in place we can now create the myapp_development database using the following command;

$ rake db:create

=> Creating database 'myapp_development'
<= db:create executed

User Accounts

To prevent just any old visitor to our site from posting content we need to have them sign in to get that kind of access, which means we’re going to need user accounts. There’s a lot of details to consider in regards to user security, but I don’t want this post to get too long, so I’m going to skip over much of those details and just post up the code. I do recommend you read the official Roda documentation and look over the sample projects to get a better understanding of how this works within the framework.

Creating the User table

Sequel comes with its own migrations DSL that we’ll use to insert a users table into the database.

# ./db/migrate/001_create_users.rb
Sequel.migration do
  change do
    create_table(:users) do
      primary_key :id, unique: true
      String :name, null: false
      String :email, unique: true, null: false
      String :password_hash, null: false
    end
  end
end

If you’ve used migrations in other frameworks such as Ruby on Rails then this should look pretty familiar. If this is new to you then here a few details to help clarify things;

  • id is set as the unique primary key (auto-incrementing); each user must have a unique id.
  • email is also set to unique as this will be used as part of the login credentials.
  • password_hash to store the users’ password (encrypted with bcrypt - see below)
  • null: false just means that these fields are required.
  • any column using unique will automatically have a database index created.

Once you’re happy with the migration you can apply that to the database with the following command;

$ rake db:migrate
<= db:migrate:up executed

(Run this command each time you create a new migration file)

User Model

Inheriting from Sequel::Model gives us all the ORM functionality directly on the User class, which leaves us to just add some validations and encryption related code;

# ./models/user.rb
class User < Sequel::Model
  attr_accessor :password, :password_confirmation

  def validate
    super
    validates_presence :password
    validates_length_range 4..40, :password
    validates_presence :password_confirmation
    errors.add(:password_confirmation, "must confirm password") if password != password_confirmation
  end

  def before_save
    super
    encrypt_password
  end

private

  def encrypt_password
    self.password_hash = BCrypt::Password.create(password)
  end
end

Neither :password nor :password_confirmation are actually saved to the database; they’re only provided for validating form data. To protect a user password (just in case our server gets hacked) we’ll encrypt passwords with BCrypt before saving them to the database. The before_save callback hands off the password to BCrypt, which is then saved to the password_hash column.

Add the Bcrypt gem to the Gemfile;

# ./Gemfile
gem "bcrypt", "~> 3.1.10"

User Routes and Views

Along with pages for viewing and creating users, we also need to have some way for them to login and logout. We should probably implement some basic authotrization too, so that only signed in users can administer accounts and post new content. There’s a lot of moving parts to this and I’m not sure this tutorials is the place to go into a lot of detail, but I will include some basic protection such as using CSRF tokens on HTML forms and by enabling user sessions using the Rack Session/Protection plugins.

In the code below you’ll notice I’ve also added the validations plugin, which will help in making sure we have entered valid form data before trying to save the record.

# ./Gemfile
gem "rack-protection", "~> 1.5.3"
gem "rack_csrf", "~> 2.5.0"

(run bundle install)

# ./myapp.rb
require "bcrypt"
require "rack/protection"

class Myapp < Roda
  Sequel::Model.plugin :validation_helpers

  use Rack::Session::Cookie, secret: "some_nice_long_random_string_DSKJH4378EYR7EGKUFH", key: "_myapp_session"
  use Rack::Protection
  plugin :csrf

  require './models/user.rb'

  # ...
end

I’m going to add login/logout routing as well as routes for user pages all in one go. It’s a lot of code but if you break it down into small pieces it should be easy to follow;

# ./myapp.rb
class Myapp < Roda
  # ...

  route do |r|
    # ...

    r.get "login" do
      view("login")
    end
    r.post "login" do
      if user = User.authenticate(r["email"], r["password"])
        session[:user_id] = user.id
        r.redirect "/"
      else
        r.redirect "/login"
      end
    end
    r.post "logout" do
      session.clear
      r.redirect "/"
    end

    r.on "users" do
      r.get "new" do
        @user = User.new
        view("users/new")
      end

      r.get ":id" do |id|
        @user = User[id]
        view("users/show")
      end

      r.is do
        r.get do
          @users = User.order(:id)
          view("users/index")
        end

        r.post do
          @user = User.new(r["user"])
          if @user.valid? && @user.save
            r.redirect "/users"
          else
            view("users/new")
          end
        end
      end
    end
  end
end

This gives us these basic routes;

  • GET /login - display log in form to visitor
  • POST /login - user submits their log in credentials
  • POST /logout - user signs out
  • GET /users/new - where a “new user” HTML form will be displayed.
  • GET /users/:id - get a user profile (:id corresponds to their primary key).
  • GET /users - listings of all users
  • POST /users - for saving a new user to the database.

When a user tries to sign in we need some way to validate their credentials, which is done via the User class;

# ./models/user.rb
def self.authenticate(email, password)
  user = filter(Sequel.function(:lower, :email) => Sequel.function(:lower, email)).first
  user && user.has_password?(password) ? user : nil
end

def has_password?(password)
  BCrypt::Password.new(self.password_hash) == password
end

private

authenticate first looks for a User with the given email and then checks that the given password matches the one stored in the database; first by using BCrypt to decrypt the stored password then running the check.

Finally it’s time to add all the views for these routes. Here’s a simple login form;

<!-- ./views/login.erb -->

<form action="/login" method="post" role="form">
  <%= csrf_tag %>
  <h2>Please Log in</h2>

  <div class="form-group">
    <label for="user_email" class="sr-only">Email address</label>
    <input type="email" name="email" id="user_email" placeholder="Email Address" class="form-control input-lg" required autofocus>
  </div>

  <div class="form-group">
    <label for="user_password" class="sr-only">Password</label>
    <input type="password" name="password" id="user_password" placeholder="Password" class="form-control input-lg" required>
  </div>

  <button class="btn btn-lg btn-primary btn-block" type="submit">Log in</button>
</form>

An index page for listing all current users and their details;

<!-- ./views/users/index.erb -->

<h1>Users</h1>
<p><a href="/users/new">Create New User</a></p>
<ul>
  <% @users.each do |user| %>
    <li><%= user.name %> (<%= user.email %>)</li>
  <% end %>
</ul>

The new view contains a form in which we can enter a users’ details (note the inclusion of a csrf_tag);

<!-- ./views/users/new.erb -->

<h1>New User</h1>

<form action="/users" method="post" role="form">
  <%= csrf_tag %>
  <div class="form-group">
    <label class="control-label" for="user_name">Name</label>
    <input class="form-control" id="user_name" name="user[name]" value="<%= @user.name %> required="required" type="text"/>
  </div>
  <div class="form-group">
    <label class="control-label" for="user_email">Email</label>
    <input class="form-control" id="user_email" name="user[email]" value="<%= @user.email %> required="required" type="email"/>
  </div>
  <div class="form-group">
    <label class="control-label" for="user_password">Password</label>
    <input class="form-control" id="user_password" name="user[password]" required="required" type="password"/>
  </div>
  <div class="form-group">
    <label class="control-label label-before" for="user_password_confirmation">Password confirmation</label>
    <input class="form-control" id="user_password_confirmation" name="user[password_confirmation]" required="required" type="password"/>
  </div>
  <div class="form-group">
    <input class="btn btn-success" type="submit" value="Create User"/>
  </div>
</form>

And lastly a simple show view;

<!-- ./views/users/show.erb -->

<h1>User Profile</h1>
<p>Name: <%= @user.name %></p>
<p>Email: <%= @user.email %></p>

When a user clicks the “logout” link, we have to send POST request to the server. If you’re feeling adventurous you could implement this using Javascript, but I’m going to do it with a plain old form. While we’re in the layout view let’s also add a couple of menu items for “login” and “users”;

<!-- ./views/layout.erb -->

<nav class="navbar navbar-default navbar-inverse">
  <div class="container">
    <ul class="nav navbar-nav">
      <li><a href="/">Home</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
      <% if session[:user_id] %>
        <li><a href="/users">Users</a></li>
        <li>
          <form action="/logout" method="post" class="navbar-form">
            <%= csrf_tag %>
            <input type="submit" value="Logout" class="btn btn-link">
          </form>
        </li>
      <% else %>
        <li><a href="/login">Login</a></li>
      <% end %>
    </ul>
  </div>
</nav>

At the moment guest visitors can administer users which is not very secure. In Roda it’s quite easy to prevent guests from accessing these features, but before we do that it’s very important for us to create a user account, otherwise we won’t be able to access them ourselves!

Go ahead, boot up the server, rackup, visit localhost:9292/users and create yourself an account, I’ll wait.

User Authentication and Authorization

Our final task on User Accounts is to restrict guest visitors from accessing user pages. For this we’ll be using Rack::Session to enable users to sign in. We’ve already provided some basic authorization when we used session[:user_id] to prevent guest visitors from seeing the Users and logout links, and we can use this same technique inside the routes.

# ./myapp.rb
route do |r|
  # ...

  unless session[:user_id]
    r.redirect "/login"
  end

  r.on "users" do
    # ...
  end
end

Simple!

As routes are processed linearly, Roda won’t process anything after this authorization check unless a session has been set, which happens when the user signs in successfully. This is going to be restrictive in a real world app, but for our purpose it should be fine.

That’s all the basic user account stuff done now. Of course there’s plenty more than could be added; perhaps a little more eye-candy; displaying form errors when the user input is wrong would be very useful; or showing “flash” messages when a new user is created successfully.

That was a lot of work so go take a short break. Make yourself a nice cup of tea and when you come back we’ll work on the task of giving registered users the ability to create posts.

Posts: Model, Routes and Views

In creating user accounts we learned all the nescessary skills for allowing registered users to write new posts. In this section we’ll create a migration, model, routing and a few view templates.

# ./db/migrate/001_create_posts.rb
Sequel.migration do
  change do
    create_table(:posts) do
      primary_key :id, unique: true
      foreign_key :user_id, :users, null: false

      String :title, null: false
      String :content, text: true, default: ""

      DateTime :created_at, null: false
      DateTime :updated_at, null: false

      index :user_id
      index :created_at
    end
  end
end

Don’t forget to apply the migrations before continuing.

There’s two main differences between the posts and users migrations

  1. We need to know which user wrote which post, so I’ve set up a foreign key constraint with user_id, along with creating an index for that column.
  2. I’ve added created_at and updated_at columns so as to keep track of when a post is created, which will be useful when sorting posts.

To improve database performance I’ve also added an index for created_at, but if you feel you need to sort posts via the modified date then you’ll also want to add an index for that column too.

Sequel comes with a useful plugin for automatically populating these timestamps on record creation;

# ./myapp.rb
class Myapp < Roda

  Sequel::Model.plugin :validation_helpers
  Sequel::Model.plugin :timestamps, update_on_create: true

  # ...
end

It’s important that we add associations between the user and post models;

# ./models/post.rb

class Post < Sequel::Model
  many_to_one :user
end

A User can have many posts, but a Post can only have one user. Don’t forget to tell User about the association as well;

# ./models/user.rb
class User < Sequel::Model
  one_to_many :posts, on_delete: :cascade
  # ...
end

If you ever need to delete a user then it’s likely you’ll want to delete their posts too, so I’ve also added the on_delete: :cascade contraint. Sequel will now automatically delete all of this user posts when their account is deleted.

The posts routing is as follows;

# ./myapp.rb
route do |r|
  # ...

  r.get /posts\\/([0-9]+)/ do |id|
    @post = Post[id]
    @user_name = @post.user.name
    view("posts/show")
  end

  unless session[:user_id]
    r.redirect "/login"
  end

  r.on "posts" do
    r.get "new" do
      @post = Post.new
      view("posts/new")
    end
    r.post do
      @post = Post.new(r["post"])
      @post.user = User[session[:user_id]]

      if @post.valid? && @post.save
        r.redirect "/"
      else
        view("posts/new")
      end
    end
  end

  # ... r.on "users"
end

Hmmm, this is a bit ugly! Our earlier decision for simple user authorization is coming back to bite us, as we’re having to split up the post routes into two separate groups; we need to allow guest visitors to view individual posts, but not create new ones, so I’ve had to straddle the session[:user_id] check. Perhaps in a later tutorial we’ll look at a cleaner and more versatile solution to authorization.

Compared to the user routes there’s a couple of differences worth noting;

  • User is assigned automatically to that which is set in the session; @post.user = User[session[:user_id]]
  • sorting is done via created_at, but in reverse order!

Notice also that we haven’t created a route for a posts listing (index). As posts are going to be displayed in a blog-like way, we don’t need to create this additional view. Just new and show views;

<!-- ./views/posts/new.erb -->

<h1>New Post</h1>

<form action="/posts" method="post" role="form">
  <%= csrf_tag %>
  <div class="form-group">
    <label class="control-label" for="post_title">Title</label>
    <input class="form-control" id="post_title" name="post[title]" value="<%= @post.title %>" required="required" type="text"/>
  </div>
  <div class="form-group">
    <label class="control-label label-before" for="post_content">Article</label>
    <textarea class="form-control" id="post_content" name="post[content]" rows="10"><%= @post.content %></textarea>
  </div>
  <div class="form-group">
    <input class="btn btn-success" type="submit" value="Create Post"/>
  </div>
</form>

And the post view;

<!-- ./views/posts/show.erb -->

<h1><%= @post.title %></h1>
<p>by <%= @user_name %> on <%= @post.created_at.strftime("%Y-%m-%d") %></p>

<%= @post.content %>

To give easy access to the new page we should add a “New Post” link in the nav bar;

<!-- ./views/layout.erb -->

<nav class="navbar navbar-default navbar-inverse">
  <div class="container">
    <ul class="nav navbar-nav">
      # ...
    </ul>

    <% if session[:user_id] %>
      <ul class="nav navbar-nav navbar-right">
        <li><a href="posts/new">New Post</a></li>
      </ul>
    <% end %>
  </div>
</nav>

Boot up the server, log in and create a test post. Then when you visit the posts page; localhost:9292/posts/1 (if that was your first post) you should see your newly created post.

Sometimes posts need to be updated so let’s create the editing routes and views next.

Editing Posts

The “edit form” is pretty much the same as the “new form”, so copy/paste that into a new file; views/posts/edit.erb change the form action parameter to /posts/<%= @post.id %>/edit and update any appropriate text labels; e.g. change “New Post” to “Edit Post”.

# ./myapp.rb
route do |r|
  # ...

  r.on "posts" do
    # ...

    r.on ":id" do |id|
      @post = Post[id]
      r.get "edit" do
        view("posts/edit")
      end
      r.post do
        if @post.update(r["post"])
          r.redirect "/posts/#{@post.id}"
        else
          view("posts/edit")
        end
      end
    end
  end
end

Give yourself a nice “edit” link in views/posts/show.erb by adding <a href="/posts/<%= @post.id %>/edit">Edit Post</a>, and don’t forget to wrap it in a session[:user_id] authorization check (take a look at the navigation bar code if you can’t remember how to do that.)

Homepage Posts Feed

Most typical blogs include a “posts feed” directly on the homepage, and ordered to show the latest post first. All that’s need to enable this for our application is to update the r.root route and adjust the homepage.erb view to iterate through a collection of posts;

# ./myapp.rb
route do |r|
  r.root do
    @posts = Post.reverse_order(:created_at)
    view("homepage")
  end
end

Here we request a reverse order collection, placing it in the @posts variable, which we can then iterate over in the homepage view;

<!-- ./views/homepage.erb -->

<% @posts.each do |post| %>
  <div class="panel panel-default">
    <div class="panel-body">
      <h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
      <p class="text-muted">by <%= post.user.name %> on <%= post.created_at.strftime("%Y-%m-%d") %></p>
      <%= post.content %>
    </div>
  </div>
<% end %>

On restarting your server you should now be able to browse around you new blog.

Final Words

We set up Roda to deliver some simple static pages, and then added User accounts and Posts to produce a simple blog-like website. There are many improvements to be made before putting this site live for the world to see, but you have enough knowledge about Roda and Sequel to move forward on your own. Here’s some ideas on what you could work on next;

  • Allow users to delete their posts (Rack::MethodOverride and the Roda plugin :all_verbs are your friends here).
  • Introduce Pagination on your homepage (using the Sequel :pagination extension).
  • Use partials to clean up your views (via the Roda :partials plugin) - the post new/edit forms would be prime candidates for this.
  • Add a check_permissions method for user authorization in routes and views.

I’ll be creating plenty of Roda tutorials over the coming months, which may well include these topics, so do check be here regularly.

I hope you found this two part tutorial useful (part one can be found here) and I’d really appreciate your feedback, so please do add your comments below.

PART 1: Up and Going in Roda: Static Ruby Websites is also available.