Skip to content

Instantly share code, notes, and snippets.

@eddanger
Last active March 7, 2018 10:08
Show Gist options
  • Save eddanger/575e251f5897f149180e to your computer and use it in GitHub Desktop.
Save eddanger/575e251f5897f149180e to your computer and use it in GitHub Desktop.
New Rails 4.2 API project with Rivets.js, Coffeescript, Sass and Bootstrap

A new Rails API project

This super lightweight Rails API project will contain the following bits of awesomeness to get you started on your new app.

Setting up the project

Create a new Rails::API (https://github.com/rails-api/rails-api) project with Postgres. Rails::API rocks for Javascript apps.

$ gem install rails-api

$ rails-api new new_project_name -T -d postgresql

Database for your data

Create the Postgres user

$ createuser -P -s -e new_project_name

Jump into the project folder and create the database

$ cd new_project_name

$ rake db:create

Assets and such

We will be using Bower to manage our front-end dependencies as it was created for this very purpose. Rubygems can probably do it, but there are a myriad of reasons it is silly! Don't worry, we will add bower-rails gem to ensure bower dependencies are compiled with Rails' asset pipeline.

$ npm install bower

Add various gems to your Gemfile

source 'https://rubygems.org'

gem 'rails', '4.2.0.rc1'

gem 'rails-api'

gem 'pg'

gem 'active_model_serializers'

gem 'bower-rails'

# Asset Poopline!
group :assets do
  gem 'coffee-script'
  gem 'sass'
end

group :test, :development do
  gem 'spring'
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  gem 'capybara'
  gem 'database_cleaner'
end

Create a Bowerfile to manage front-end dependencies

asset 'bootstrap-sass-official'
asset 'rivets'
# vim: ft=ruby

Let's finally bundle this and install things

$ bundle install

$ rails g rspec:install

$ rake bower:install

Get the asset pipeline all ready... with a barebones Rivets app.

$ mkdir app/assets/javascripts

$ touch app/assets/javascripts/application.js

//= require jquery
//= require sightglass
//= require rivets

//= require app

$ touch app/assets/javascripts/app.coffee

# Hello app!

$ ->
  greeting =
    heading: 'Hello!'
    blurb: 'Now we are ready to get this show on the road.'

  rivets.bind $('.hello'),
    greeting: greeting

$ mkdir app/assets/stylesheets

$ touch app/assets/stylesheets/application.css.scss

/*
 * This is a manifest file that'll automatically include all the stylesheets available in this directory
 * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
 * the top of the compiled file, but it's generally better to create a new file per style scope.
 *= require_self
 *= require_tree .
*/

@import "bootstrap-sass-official/assets/stylesheets/bootstrap-sprockets";
@import "bootstrap-sass-official/assets/stylesheets/bootstrap";

Add a controller, view and route that ties the room together!

$ touch app/controllers/home_controller.rb

class HomeController < ApplicationController
  def index
    render :index
  end
end

$ mkdir -p app/views/home

$ touch app/views/home/index.html.erb

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Hello!</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
</head>
<body>

  <div class="container">
    <div class="hello text-center">
      <h1>{ greeting.heading }</h1>
      <p class="lead">{ greeting.blurb }</p>
    </div>
  </div>

</body>
</html>

Update config/routes.rb

Rails.application.routes.draw do
  root 'home#index'
end

Try and run this mofo now!

$ rails s

Go to http://localhost:3000 and make sure everything is working.

Commit the project

If all is to your liking let's commit this. The best it yet to come!

Update your .gitignore file with this

# Ignore stuff
/node_modules

$ git init

$ git add .

$ git commit

A new Rails API project (Backbone.js and authentication)

Part 2 a work in progress...

Add underscore, backbone to Bowerfile

asset 'underscore'
asset 'backbone'

Add underscore, backbone to application.js

//= require underscore
//= require backbone

Warden auth for Rails API app

Password digest built into rails. http://robert-reiz.com/2014/04/12/has_secure_password-with-rails-4-1/

Create the User model

rails g model user first_name:string last_name:string email:string password_digest:string

class User < ActiveRecord::Base
  has_secure_password

  validates :first_name, :last_name, :email, presence: true
  validates :email, uniqueness: true
  validates :password, presence: true, on: :create
  validates :password, :length => { :minimum => 5 }
end

User spec and User Factory...

in Gemfile

gem 'warden'
gem 'rails_warden'
gem 'bcrypt'

in config/initializers/warden.rb

Rails.configuration.middleware.use RailsWarden::Manager do |manager|
  manager.default_strategies :user
  #manager.failure_app = LoginController
  manager.failure_app = lambda { |env| [302, {'Content-Type' => 'text/html; charset=utf-8', 'Location' => '/login'}, ['']] }


  manager.serialize_into_session do |obj|
    {:class => obj.class, :id => obj.id}
  end

  manager.serialize_from_session do |obj|
    return obj[:class].find(obj[:id])
  end
end

module Strategies
  class User < Warden::Strategies::Base
    def valid?
      Rails.logger.info "#{params['email']} + #{params['password']}"
      params['email'] && params['password']
    end

    def authenticate!
      user = ::User.where(:email => params['email'].downcase).first

      Rails.logger.info "+ Authenticating user #{params['email']}"

      if user && user.valid_password?(params['password'])
        success!(user)
      else
        fail!("Could not authenticate you.")
      end

      Rails.logger.info "+ Authenticating done"
    end
  end
end

Warden::Strategies.add(:user, Strategies::User)

Add lib/warden.rb

module AttendeaseWarden
  module Mixins
    module HelperMethods
      # The main accessor for the warden proxy instance
      # :api: public
      def warden
        request.env['warden']
      end

      # Proxy to the authenticated? method on warden
      # :api: public
      def authenticated?(scope = :default)
        warden.authenticated?(scope)
      end

      # Access the currently logged in user
      # :api: public
      def current_user(scope = :default)
        warden.user(scope)
      end

      def current_user=(user)
        #if user.kind_of?(Attendee)
        #  warden.set_user(user, :scope => user.event.subdomain)
        #else
          warden.set_user(user)
        #end
      end
    end # Helper Methods

    module ControllerOnlyMethods
      # Logout the current user
      # :api: public
      def logout!(*args)
        warden.raw_session.inspect  # Without this inspect here.  The session does not clear :|
        warden.logout(*args)
      end

      # Proxy to the authenticate method on warden
      # :api: public
      def authenticate(*args)
        warden.authenticate(*args)
      end

      # Proxy to the authenticate method on warden
      # :api: public
      def authenticate!(*args)
        warden.authenticate!(*args)
      end
    end
  end
end

Load stuff from /lib Add this to your application.rb

    # Custom directories with classes and modules you want to be autoloadable.
    config.autoload_paths += %W(#{config.root}/lib)

Add app/helpers/application_helper.rb

module ApplicationHelper
  include AttendeaseWarden::Mixins::HelperMethods
end

Add this to apps/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include AttendeaseWarden::Mixins::HelperMethods
  include AttendeaseWarden::Mixins::ControllerOnlyMethods

  class ClientError               < StandardError; end
  class UnauthenticatedError      < ClientError; end
  class UnauthorizedError         < ClientError; end
  class NotFoundError             < ClientError; end

  rescue_from UnauthenticatedError,           :with => :unauthenticated   # 401 Unauthorized
  rescue_from UnauthorizedError,              :with => :unauthorized      # 403 Forbidden
  rescue_from ActionController::RoutingError, :with => :not_found         # 404 Not Found
  rescue_from ActionView::MissingTemplate,    :with => :not_found         # 404 Not Found

  # Display error when user is unauthenticated to access resource
  #
  # @return [String, nil]
  #   the content to display
  #
  # @api semipublic
  def unauthenticated
    params['return_url'] = "#{request.protocol}#{request.host_with_port}#{request.fullpath}"
    render_error :unauthenticated, 401, warden.message || "Could not authenticate you."
  end

  # Display error when user is unauthorized to access resource
  #
  # @return [String, nil]
  #   the content to display
  #
  # @api semipublic
  def unauthorized
    render_error :unauthorized, 403, "You do not have the correct permissions to access this section."
  end


  # Display error when resource not found
  #
  # @return [String, nil]
  #   the content to display
  #
  # @api semipublic
  def not_found
    render_error :not_found, 404, "Resource could not be found."
  end


  # -- Private Methods --------------------------------------------------

  private

  # Authenticate the user
  #
  # @return [nil]
  #   nil if successful
  #
  # @raise [UnauthenticatedError]
  #   raised if the user is not authenticated
  #
  # @api semipublic
  def authenticate!
    authenticate(:user)

    raise UnauthenticatedError unless current_user
  end

  # Authorizes the user to access resource
  #
  # @return [nil]
  #   nil if successful
  #
  # @raise [UnauthorizedError]
  #   raised if the user is unauthorized
  #
  # @api semipublic
  def authorize!
    # only a member in the organization owner team can view/manage the organization
    if controller_name == 'organizations' && @organization && ['show', 'update', 'delete'].include?(action_name)
      raise UnauthorizedError unless current_user && @organization.owner?(current_user)
    end
  end


  # Display error template with http status
  #
  # @return [String, nil]
  #   the content to display
  #
  # @api private
  def render_error(name, status, message, fields = nil)
    Rails.logger.info "+ Rescued from exception: #{name} #{status}"

    warden.custom_failure! if status == 401

    if fields
      # Fields is just a JSON string attached to the exception.
      # It lists all the field error messages.
      render :json => {:error => message, :errors => ActiveSupport::JSON.decode(fields)}, :status => status
    else
      render :json => {:error => message}, :status => status
    end
  end

end

Update HomeController

class HomeController < ApplicationController
  def index
    if authenticated?
      render :app
    else
      render :login
    end
  end
end

Add /app/views/home/login.html.erb

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Hello!</title>
  <%= stylesheet_link_tag    'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
</head>
<body>

  <div class="app"></div>

  <script>
    Crew.app = new Crew.App();

    Backbone.history.start({ pushState: true, root: "/" });
  </script>

</body>
</html>

Create AccountsController

class AccountsController < ApplicationController
  before_filter :authenticate!, :except => [ :login, :logout ]
  before_filter :authorize!,    :except => [ :login, :logout ]

  def show
    render :json => current_user
  end

  def update_password
    @user = current_user

    old_password = params[:old_password]
    new_password = params[:new_password]

    if old_password == '' or new_password == ''
      flash[:error] = I18n.t('accounts.password_blank')
    elsif @user.valid_password?(old_password)
      flash[:error] = I18n.t('accounts.password_wrong')
    else
      @user.update_attributes(:password => new_password)
      flash[:notice] = I18n.t('accounts.password_update_successful')
    end

    respond_with @user, :location => :account
  end

  def verify_credentials
    if authenticated?
      render :json => current_user
    else
      raise UnauthenticatedError unless current_user
    end
  end

  def login
    authenticate(:user)

    if authenticated?
      render :json => current_user
    else
      raise UnauthenticatedError unless current_user
    end
  end

  def logout
    logout!(:default)

    redirect_to :root, :notice => "Signed Out"
  end

end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment