Skip to content

old style milia app tutorial

Daudi Amani edited this page Dec 11, 2013 · 1 revision

Manually setting up a multi-tenanting app

milia v1.0.0-beta-3 and higher releases now rely on generators to do all of this

I'm leaving this tutorial here for historic reference purposes; but for most applications, you should rely on the generators to automatically set up devise and an integrated milia. If you ever wonder why something was done, however, this document should help answer your questions.

using rails, devise, & milia

This tutorial is a capture of everything I did to create a sample app for milia. There's enough brief comments for anyone to follow step-by-step. It is based on my dev environment which is Ubuntu 13.10 on a PC. YMMV.

_This tutorial is based on milia version v1.0.0-beta-2 (latest is beta-2), so you _ probably should also reference the current doc/sample.sh (my running notes when testing the sample-milia-app) for any changes or latest info!

The "app" itself is merely a simple barebones structure to display an index page, require sign-in to do anything else, has a sign-up page for starting a new organization (ie tenant), a way to send invitations to other members, and a single tenanted model (members) to prove that tenanting is working.

you can see an exact copy of the sample on github: https://github.com/dsaronin/sample-milia-app

Resources

  • doc/sample.sh -- will ALWAYS be the most recent of my notes for creating a sample app (for example in the edge branch: "newdev")
  • github.com/milia/wiki/sample-milia-app-tutorial (this document) this should be the same as the sample.sh doc for the current stable release (or latest beta version); but markdown formatted https://github.com/dsaronin/milia/wiki/sample-milia-app-tutorial
  • milia README:
    • this will be the knowledgable programmer's digest of the essentials
    • thus it won't cover some of the intricacies of actually implementing milia: either the tutorial or sample.sh will do that
    • if you're a first time milia implementer, please use both the README and either of the two above documents for assistance: it will save you time.

FEEDBACK

If you run into difficulties while following the steps here, please be sure to reference the LINE NUMBER or SECTION of the point at which had a problem, as well as any output from that step.

BUT (caveat)

if you've gone commando and have been making changes & enhancements OR have been trying to roll out a full app, you're more or less on your own. I strongly recommend experimenting with milia first in this simple format, get it working, then ADD in increasing layers of complexity and difficulty. Trying to make too many changes at once is a recipe for difficulty in troubleshooting. Don't expect to get rescued.

NOTES

This file isn't fully executable as a shell script. There are just too many things you'll have to to do to help things along.

  1. Instructions for you to do things are in regular text; things you should type or cut&paste, will be in pre-formatted monospaced fonts. commands preceded by a "$" prompt indicate shell level command. commands preceded by a ">" prompt indicate some other program command. in either case, don't type the prompt as part of the command!
  2. I've bracketed groups of text to be edited/added to a file with the following style:

    EDIT: <path/filename>

    things to do &/or edit or add

    ADD: stuff to add follows

    things to add
  3. follow everything exactly in the order given
  4. there's non-milia related stuff, if you'll be using heroku to host; treat this as optional, if you'd like. but at least I know it works as a completed app.

STEP 0 - PREREQUISITES & EXPECTED BACKGROUND PREPARATION

This background is what I've done on my Ubuntu dev workstation so if you want to follow exactly, you'll need similar. None of this is necessarily required for milia; only to exactly bring up this sample-milia-app.

Make sure you have your ssh keys gen'd

  $ ssh-keygen

Make sure you have some basic packages on your system

  $ sudo apt-get install curl git vim vim-gnome

Make sure you've set up a github account, and git globals on your workstation.

Install RVM on your system. See http://rvm.io for more information and do any adjustments to your .bashrc, etc files as needed,

  $ \curl -L https://get.rvm.io | bash -s stable

Make sure to install ruby 2.0.0

  $ rvm install 2.0.0

I have all my projects in a directory called "projectspace", so these instructions start there.

  $ mkdir projectspace
  $ rvm gemset create projectspace
  $ echo "projectspace" > projectspace/.ruby-gemset
  $ echo "2.0.0" > projectspace/.ruby-version
  $ cd projectspace

install rails (latest version)

  $ gem install rails

OPTIONAL -- get ready for using heroku

  • set up a heroku account at: http://heroku.com
  • install heroku toolbelt: heroku, foreman
  $ wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh
  $ heroku login

set environment variable for later Procfile and later recaptcha usage; I put them in .bashrc.

export PORT=3000
export RACK_ENV=development
# OPTIONAL: recaptcha keys
export RECAPTCHA_PUBLIC_KEY=6LeYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKpT
export RECAPTCHA_PRIVATE_KEY=6LeBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBgQBv

STEP 1 - CREATION OF SKELETON APP & REPOSITORY

@GITHUB: create a new repository <your-new-app> for <git-user> (you) anywhere below where you see sample-milia-app, change it to <your-new-app>.

  $ cd projectspace   # if not there already

  $ rails new sample-milia-app
  $ echo "sample" > sample-milia-app/.ruby-gemset
  $ echo "2.0.0" > sample-milia-app/.ruby-version
  $ echo "web: bundle exec thin start -R config.ru -p $PORT -e $RACK_ENV" > sample-milia-app/Procfile
  $ rvm gemset create sample
  $ cd sample-milia-app
  $ git init
  $ git add --all .
  $ git commit -am 'initial commit'
  $ git remote add origin [email protected]:<git-user>/sample-milia-app.git
  $ git push -u origin master

STEP 2 - SET UP GEMFILE, BUNDLE INSTALL GEMS

change .gitignore to match your development environment:

I just copy my standard .gitignore from another project (as in sample below), but you can copy mine from sample-milia-app on github.

  $ cp ../<somewhere else>/.gitignore .

EDIT Gemfile

  $ vim Gemfile

First, comment OUT the turbolinks gem

  # gem 'turbolinks'

then, enable rubyracer in Gemfile by de-commenting

  gem 'therubyracer', platforms: :ruby

finally, ADD the following lines to Gemfile

  ruby "2.0.0"   # heroku likes this at the head, as line 2

  # =========================================================
  # sample-milia-app specific stuff
  # =========================================================
  # Bundle the extra gems:
  gem 'haml-rails'   
  gem 'html2haml', :git => 'git://github.com/haml/html2haml.git'  # "2.0.0.beta.2", 

  # stuff that heroku likes to have
  gem 'thin'
  gem "SystemTimer", :require => "system_timer", :platforms => :ruby_18
  gem "rack-timeout"
  gem 'rails_12factor'

  # airbrake is optional and configured by config.use_airbrake in milia initializer
  # default is false; if you change it to true, uncomment out the line below
  # gem 'airbrake'   # uncomment this if you will use airbrake for exception notifications

  gem 'web-app-theme', :git => 'git://github.com/dsaronin/web-app-theme.git'
  gem 'devise', '~>3.2'
  gem 'milia', :git => 'git://github.com/dsaronin/milia.git', :branch => 'v1.0.0-beta-2'

  # recaptcha is optional and configured by config.use_recaptcha in milia initializer
  # default is true; if you change it to false, comment out the line below
  gem 'recaptcha', :require => "recaptcha/rails"

EDIT: app/assets/javascripts/application.js

comment out turbolinks in your Javascript manifest file; we won't need turbolinks for this simple sample.

  //  require turbolinks to 

BUNDLE install all the gems

  $ bundle install

STEP 3 - PREP APP UI TEMPLATES & CHECK OUT DISPLAYS

Source for web-app-theme related notes and revisions: http://blog.bryanbibat.net/2011/09/24/starting-a-professional-rails-3-1-app-with-web-app-theme-devise-and-kaminari/

Generate home page

  $ rails g controller home index

remove default Rails index page

  $ rm public/index.html

EDIT config/routes.rb

ADD the root :to => "home#index" within the do..end block

    SampleMiliaApp::Application.routes.draw do

      root :to => "home#index"

    end

create the database

  $ rake db:create

test by starting server:

  $ foreman start

CHECK-OUT: at your browser:

  http://localhost:3000/

you should see an empty template page for home/index

GENERATE A THEME with web-app-theme gem

  $ rails g web_app_theme:theme --engine=haml --theme="red" --app-name="Simple Milia App"

Delete the default layout originally generated

  $ rm app/views/layouts/application.html.erb

generate some sample text for the page to flesh it out

  $ rails g web_app_theme:themed home --themed-type=text --theme="red" --engine=haml

  $ mv app/views/home/show.html.haml app/views/home/index.html.haml

STEP 4 - SIMPLE devise SET UP (pre-installing milia)

  $ rails g devise:install
  $ rails g devise user

EDIT: config/environments/development.rb

ADD: following AFTER the final config.action_xxxxx stuff

(of course, you will want to change your domain, email user_name and password to match your actual values!)

  # devise says to define default url
  config.action_mailer.default_url_options = { :host => 'localhost:3000' }

  # set up for email sending even in dev mode
  # Don't care if the mailer can't send
  config.action_mailer.raise_delivery_errors = false

  config.action_mailer.delivery_method = :smtp
  
  ActionMailer::Base.smtp_settings = {
    :address => "smtp.gmail.com",
    :port => "587",
    :authentication => :plain,
    :user_name => "[email protected]",
    :password => "my-password",
    :enable_starttls_auto => true
  }

EDIT: config/environments/production.rb

this sample is showing as how it would be if your production server is hosted via heroku.com using the SENDGRID plugin for emailing.

ADD: following AFTER the final config.action_xxxxx stuff

  # devise says to define default url
  config.action_mailer.default_url_options = { :host => 'secure.simple-milia-app.com', :protocol => 'https' }

  ActionMailer::Base.delivery_method = :smtp

  ActionMailer::Base.smtp_settings = {
    :address        => 'smtp.sendgrid.net',
    :port           => '587',
    :authentication => :plain,
    :user_name      => ENV['SENDGRID_USERNAME'],
    :password       => ENV['SENDGRID_PASSWORD'],
    :domain         => 'heroku.com'
  }

EDIT: config/environments/test.rb

ADD: following AFTER the final config.action_xxxxx stuff

  # devise says to define default url
  config.action_mailer.default_url_options = { :host => "www.example.com" }

set up scopes for devise

EDIT: app/models/user.rb

add confirmable to line 4

  devise :database_authenticatable, :registerable, :confirmable,

EDIT: db/migrate/xxxxxxx_devise_create_users.rb

uncomment the confirmable section, it will then look as follows:

      ## Confirmable
      t.string   :confirmation_token
      t.datetime :confirmed_at
      t.datetime :confirmation_sent_at
      t.string   :unconfirmed_email # Only if using reconfirmable

and uncomment the confirmation_token index line

    add_index :users, :confirmation_token,   :unique => true

EDIT: config/initializers/devise.rb

change mailer_sender to be your from: email address

  config.mailer_sender = "[email protected]"

OPTIONAL: locate and uncomment the following lines (in various places):

  config.pepper = '46f2....'
  config.confirmation_keys = [ :email ]
  config.email_regexp = /\A[^@]+@[^@]+\z/

run the migration

  $ rake db:migrate

CHECK-OUT: check things out at browser before proceeding

stop/restart foreman: ^c stops foreman; foreman start restarts it; F5 refreshes the browser page

customize login screen

generate the sign-in/sign-out layout:

  $ rails g web_app_theme:theme sign --layout-type=sign --theme="red" --engine=haml --app-name="Simple Milia App"

EDIT: config/application.rb

NOTE: please see details and cautions at: http://guides.rubyonrails.org/asset_pipeline.html Section 4.1 Precompiling Assets

uncomment the config.time_zone line and set it to your timezone

    config.time_zone = 'Pacific Time (US & Canada)'

IF: you will be deploying production on heroku, then

ADD: following AFTER the config.time_zone line

    #  For faster asset precompiles, you can partially load your application. 
    #  In that case, templates cannot see application objects or methods. 
    #  Heroku requires this to be false.
    config.assets.initialize_on_precompile = false

change the layout for sign-in/sign-up

by adding the following into the class .... end block

  config.to_prepare do
    Devise::SessionsController.layout "sign"
    Devise::RegistrationsController.layout "sign"
    Devise::ConfirmationsController.layout "sign"
    Devise::PasswordsController.layout "sign"
   end

Preparing the sign in/up/out etc views

if we use devise to gen the views, they'll be genned in erb and a different format from the layout style we're using. my web_app_theme has a generator to gen them automagically

  $ rails g web_app_theme:devise

EDIT: app/controllers/application_controller.rb

NOTE: this line is only for the basic devise (no milia) version; we will later uncomment or remove this line when we install milia

ADD following lines immediately after line 4 protect_from_forgery ...

  before_action :authenticate_user!

EDIT: app/controllers/home_controller.rb

ADD immediately after line 1 class HomeController

  skip_before_action :authenticate_user!, :only => [ :index ]

STEP 4 - TEST devise SIGN UP, ACTIVATION, SIGN IN, SIGN OUT

NOTE: we will later DELETE all users added in this manner BEFORE we install milia. Reason is because currently there is no tenanting. DO NOT TRY TO LATER MANUALLY ATTEMPT TO CONVERT THESE INITIAL USERS TO A TENANTING MODEL: it is poor software practice to do that. you need to be testing and verifying that we've got devise up and enabled by using the devise-based sign up, confirm, sign in process.

CHECK-OUT:

  • sign up as a new user,
  • the log file will show that an email was sent together with the activation code & URL and if your email/password are correct, an email should have been sent as well!
  • copy & paste this address as-is into the browser address area & go to it to activate
  • it will take you to a sign in screen; sign in
  • REFRESH index page (to refresh the logout validity token)
  • sign out
  • sign in again as the user

STEP 5 - adding in milia and making multi-tenantable

remove any users created above in STEP 4

start the rails console

  $ rail c
    > User.all.each{|x| x.destroy}
    > exit

rollback the initial migration (because we'll be changing it slightly)

  $ rake db:rollback

Milia expects a user session, so please set one up

EDIT: Gemfile

ADD

  gem 'activerecord-session_store', github: 'rails/activerecord-session_store'

BUNDLE install to get the new gems

  $ bundle install

now generate the session migration

  $ rails g active_record:session_migration

EDIT: db/migrate/xxxxxxx_devise_create_users.rb

add above the t.timestamps line:

      # milia member_invitable
      t.boolean    :skip_confirm_change_password, :default => false

      t.references :tenant

generate the tenant migration

  $ rails g model tenant tenant:references name:string:index

generate the tenants_users join table migration

  $ rails g migration CreateTenantsUsersJoinTable tenants users

EDIT: db/migrate/20131119092046_create_tenants_users_join_table.rb

then uncomment the first index line as follows:

      t.index [:tenant_id, :user_id]

EDIT: app/controllers/application_controller.rb

NOTE: before all tenanted controllers, you MUST HAVE a

     before_action :authenticate_tenant!

It is best to have it at the start of your application_controller If you happen to have any general universal access controllers, then you can place at the top of those specific controllers:

     skip_before_action :authenticate_tenant!, :only => [ <action name>  ]

CHANGE: the authenticate_user! line to authenticate_tenant!

(make it look like the statement below)

  before_action :authenticate_tenant!   # authenticates user and sets up tenant

ADD following lines immediately after that:

  rescue_from ::Milia::Control::MaxTenantExceeded, :with => :max_tenants
  rescue_from ::Milia::Control::InvalidTenantAccess, :with => :invalid_tenant
# milia defines a default max_tenants, invalid_tenant exception handling
# but you can override if you wish to handle directly

EDIT: config/routes.rb

ADD the :controllers clause to the existing devise_for :users

  as :user do   #   *MUST* come *BEFORE* devise's definitions (below)
    match '/user/confirmation' => 'milia/confirmations#update', :via => :put, :as => :update_user_confirmation
  end

  devise_for :users, :controllers => { 
    :registrations => "milia/registrations",
    :confirmations => "milia/confirmations",
    :sessions => "milia/sessions", 
    :passwords => "milia/passwords", 
  }

EDIT: app/models/user.rb

ADD after the class User line:

    acts_as_universal_and_determines_account

EDIT: app/models/tenant.rb

DELETE

  belongs_to  :tenant

ADD after the class Tenant line:

    acts_as_universal_and_determines_tenant

  def self.create_new_tenant(tenant_params, coupon_params)

    tenant = Tenant.new(:name => tenant_params[:name])

    if new_signups_not_permitted?(coupon_params)

      raise ::Milia::Control::MaxTenantExceeded, "Sorry, new accounts not permitted at this time" 

    else 
      tenant.save    # create the tenant
    end
    return tenant
  end

  # ------------------------------------------------------------------------
  # new_signups_not_permitted? -- returns true if no further signups allowed
  # args: params from user input; might contain a special 'coupon' code
  #       used to determine whether or not to allow another signup
  # ------------------------------------------------------------------------
  def self.new_signups_not_permitted?(params)
    return false
  end

  # ------------------------------------------------------------------------
  # tenant_signup -- setup a new tenant in the system
  # CALLBACK from devise RegistrationsController (milia override)
  # AFTER user creation and current_tenant established
  # args:
  #   user  -- new user  obj
  #   tenant -- new tenant obj
  #   other  -- any other parameter string from initial request
  # ------------------------------------------------------------------------
    def self.tenant_signup(user, tenant, other = nil)
      #  StartupJob.queue_startup( tenant, user, other )
      # any special seeding required for a new organizational tenant
    end

EDIT: app/controllers/home_controller.rb

CHANGE skip_authenticate_user! to skip_authenticate_tenant!

  skip_before_action :authenticate_tenant!, :only => [ :index ]

REPLACE the empty def index ... end with following ADD:

this will give you improved handling for letting user know what is expected. If you want to have a welcome page for signed in users, uncomment the redirect_to line, etc.

  def index
    if user_signed_in?

        # was there a previous error msg carry over? make sure it shows in flasher
      flash[:notice] = flash[:error] unless flash[:error].blank?
      #   redirect_to(  welcome_path()  )

    else

      if flash[:notice].blank?
        flash[:notice] = "sign in if your organization has an account"
      end

    end   # if logged in .. else first time

  end

run the migration

  $ rake db:migrate

config/initializers/milia.rb now supported for config parameters

OPTIONAL: change milia configuration options

To do that, copy doc/milia-initializer.rb to config/initializers/milia.rb

edit values as appropriate.

NOTE: if Milia.use_coupon is true (default configuration option), then your sign up form MUST return a parameter

     :coupon => { :coupon => <string> } 

which can also be blank.

OPTIONAL: edit config/application.rb and add the following to alter

default behavior for handling strong_parameters in Rails see: https://github.com/rails/strong_parameters#handling-of-unpermitted-keys choose one of the two options: :raise OR :log

 ActionController::Parameters.action_on_unpermitted_parameters = :raise | :log

CHECK-OUT: restart foreman and check out at your browser:

  http://localhost:3000/
  • click sign up to sign up a new account
  • get confirmation email (or view in log)
  • activate the new account, sign in, sign out, etc.

STEP 6 - adding a tenanted members table, then inviting a member

REMOVE any users, and tenants created above in STEP 5

Using the rails console:

  $ rails c
    > User.all.each{|x| x.destroy}
    > Tenant.all.each{|x| x.destroy}
    > exit

GENERATE some type of tenanted member table:

  $ rails g resource member tenant:references user:references first_name:string last_name:string 

ADD to app/models/tenant.rb

  has_many :members, dependent: :destroy

EDIT self.tenant_signup method and insert the Member.create_org_admin line

  # ------------------------------------------------------------------------
    def self.tenant_signup(user, tenant, other = nil)
      #  StartupJob.queue_startup( tenant, user, other )
      # any special seeding required for a new organizational tenant

      Member.create_org_admin(user)
    end

EDIT app/models/user.rb

ADD

    has_one :member, :dependent => :destroy

EDIT app/models/member.rb

REMOVE belongs_to :tenant

ADD

  acts_as_tenant

  DEFAULT_ADMIN = {
    first_name: "Admin",
    last_name:  "Please edit me",
    favorite_color: "blue"
  }

  def self.create_new_member(user, params)
    # add any other initialization for a new member
    return user.create_member( params )
  end

  def self.create_org_admin(user)
    new_member = create_new_member(user, DEFAULT_ADMIN)
    unless new_member.errors.empty?
      raise ArgumentError, new_member.errors.full_messages.uniq.join(", ")
    end

    return new_member
      
  end

EDIT: app/views/members/new.html.haml

%h1 Simple Milia App
.block#block-signup
  %h2 Invite a new member into #{@org_name}
  .content.login
    .flash
      - flash.each do |type, message|
        %div{ :class => "message #{type}" }
          %p= message
    - flash.clear  # clear contents so we won't see it again

    = form_for(@member, :html => { :class => "form login" }) do |f|
      - unless @member.errors.empty? && @user.errors.empty?
        #errorExplanation.group
          %ul
            = @member.errors.full_messages.uniq.inject(''){|str, msg| (str << "<li> #{msg}") }.html_safe
            = @user.errors.full_messages.uniq.inject(''){|str, msg| (str << "<li> #{msg}") }.html_safe

      = fields_for( :user ) do |w|
        .group
          = w.label :email, :class => "label "
          = w.text_field :email, :class => "text_field"
          %span.description Ex. test@example.com; must be unique

      .group
        = f.label :first_name, :class => "label "
        = f.text_field :first_name, :class => "text_field"

      .group
        = f.label :last_name, :class => "label "
        = f.text_field :last_name, :class => "text_field"

      .group
        = f.label :favorite_color, :class => "label "
        = f.text_field :favorite_color, :class => "text_field"
        %span.description What is your favorite color?

      .group.navform.wat-cf
        %button.button{ :type => "submit" }
          = image_tag "web-app-theme/icons/key.png"
          Create user and invite

EDIT app/controllers/application_controller.rb

ADD:

  before_action  :prep_org_name

private

# org_name will be passed to layout & view
  def prep_org_name()
    @org_name = ( user_signed_in?  ?
      Tenant.current_tenant.name  :
      "Simple Milia App"
    )

  end

EDIT app/views/layouts/application.rb

The following is not a requirement, but serves to show how to handle tenanted sign ins and welcome pages replaces the two instances of "Simple Milia App" with (everything between the quotes but not including the quotes): "= @org_name", make the results look like the two lines below

  %title= @org_name

  = link_to @org_name, "/"

make changes to layout for invite member; change the portion

with the "sign_up" link to look like the following:

              %li
                - if user_signed_in?
                  = link_to t("web-app-theme.invite", :default => "Invite member"), new_member_path
                - else
                  = link_to( t("web-app-theme.signup", :default => "Sign up"), new_user_registration_path )

EDIT: app/controllers/members_controller.rb

ADD after the class line:

  layout  "sign", :only => [:new, :edit, :create]

  def new()
    @member = Member.new()
    @user   = User.new()
  end

  def create()
    @user   = User.new( user_params )

    # ok to create user, member
    if @user.save_and_invite_member() && @user.create_member( member_params )
      flash[:notice] = "New member added and invitation email sent to #{@user.email}."
      redirect_to root_path
    else
      flash[:error] = "errors occurred!"
      @member = Member.new( member_params ) # only used if need to revisit form
      render :new
    end

  end


  private

  def member_params()
    params.require(:member).permit(:first_name, :last_name, :favorite_color)
  end

  def user_params()
    params.require(:user).permit(:email, :password, :password_confirmation)
  end

RUN the migration

$ rake db:migrate

CHECK-OUT: check things out at browser before proceeding

stop/restart foreman
  • you will have to first sign-up, confirm, then you can invite_member
  • sign-out, confirm new member, etc
Clone this wiki locally