Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 303 additions & 0 deletions content/guides/set-up-rails-appplication-with-omniauth-bookingsync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
# Building New Rails Application With Devise And BookingSync Integration

If you need to integrate your existing Rails application (which most likely uses Devise for authentication) with BookingSync, that's the exact guide you are looking for.

## Setup

After setting up environment for Ruby (check either [rvm](https://rvm.io) or [rbenv](https://github.com/rbenv/rbenv)), install `bundler` and `rails` gems:

~~~
gem install bundler
gem install rails
~~~

And generate new Rails application:

~~~
rails new omniauth-bookingsync-app
cd omniauth-bookingsync-app
~~~

To finish the setup we just need to create the database:

~~~
bundle exec rake db:create
~~~

To verify it works, just run:

~~~
rails server
~~~

And visit `http://localhost:3000`.


## Adding Devise

Add [`devise`](https://github.com/plataformatec/devise) gem to Gemfile:

~~~ Gemfile
gem 'devise'
~~~

And run:

~~~
bundle install
~~~

Then run the generator:

~~~
rails generate devise:install
rails generate devise:views
~~~

We don't need to have a signup / signin alternative with BookingSync, just a way to integrate the account, so we can remove this part from `app/views/devise/shared/_links.html.erb`:

~~~ html app/views/devise/shared/_links.html.erb
<%- if devise_mapping.omniauthable? %>
<%- resource_class.omniauth_providers.each do |provider| %>
<%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
<%- end -%>
<% end -%>
~~~
Copy link

@syedsanahassan syedsanahassan Mar 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really a comment, it's a part of the docs, just not sure why it doesn't really work for html / erb.


Another step would be generating a model for Devise, for the sake of simplicity let's call it `Account` to match the concept from BookingSync authentication resource:

~~~
rails generate devise account
~~~

The default migration generated by that command should be sufficient for this example app. Now you can run the migration:

~~~
rake db:migrate
~~~

## Adding OmniAuth and integrating BookingSync

Go to the [BookingSync Partners section](https://www.bookingsync.com/en/partners/login) and register you account or sign in if you already have one. After you sign in, create a new application. You will need to the provide a name, an admin URL (the URL to which the user should be redirected to after signing in when developing `embedded` application) and a redirect URI. Here's an example:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

register you ... > your


<div>
<img alt="BookingSync" src="/images/bookingsync_new_application.png">
</div>

In this case we want to have a separate application, not the one that should be accessible only from the applications' store, so `standalone` application is the way to go.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why, no reasons to have a standalone application if you can embedded it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you want to have embedded application? If it's separate from BookingSync ecosystem, you would probably want to have it standalone instead of embedding, no? Or maybe I don't get the use case, but can't really think why it would be better to have embedded application here.

Copy link
Member

@ZenCocoon ZenCocoon Apr 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, the UX of users is greatly improved with Embedded applications. This keeps your management on single window and keep BookingSync sidebar small on the left.


Remember about providing data for both **en** and **fr** locales. You will also need to install this application on your BookingSync account using `Private App Access Code` in the applications' store.

Now add [omniauth-bookingsync](https://github.com/BookingSync/omniauth-bookingsync) gem to your Gemfile:

~~~
gem 'omniauth-bookingsync'
~~~

And create the initializer (e.g. `config/initializers/omniauth.rb`) for OmniAuth:

~~~ ruby
Rails.application.config.middleware.use OmniAuth::Builder do
provider :bookingsync, ENV["BOOKINGSYNC_APPLICATION_ID"], ENV["BOOKINGSYNC_SECRET"], scope: "public rentals_read"
end
~~~

For the sake of example we added extra `rentals_read` scope besides `public` which is always included. You can learn more about available scopes [here](http://developers.bookingsync.com/reference/authorization/#scopes).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public is not included by default anymore, but SHOULD be requested on the scopes list.


To avoid hardcoding sensitive data, you can use [dotenv](https://github.com/bkeepers/dotenv) gem.

You can get both the application and app secret in partners section.

Now we need to generate some migrations for handling authentication process. To keep it simple here, we will add it to the `Account` model directly, but you will probably want to have a separte to-one relationship with other model like `BookingSyncAcount`. The details how the process works is beyond the scope of this guide, you can learn more about it [here](https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separte > separate


~~~
rails g migration AddOAuthFieldsToAccounts provider:string uid:integer:index \
name:string oauth_access_token:string oauth_refresh_token:string \
oauth_expires_at:string
rake db:migrate
~~~

Now, make your `Account` model omniauthable:

~~~ ruby app/models/user.rb
devise :omniauthable, omniauth_providers: [:bookingsync]
~~~

The next step would be authenticating and integrating BookingSync account. But to integrate the existing account, we need to be signed in already in the first place. Let's create some `admin` page that requires user to be logged in. First, let's add it to the routes, making it also a root path:

~~~ ruby config/routes.rb
Rails.application.routes.draw do
root to: "admin#index"

devise_for :accounts, controllers: { omniauth_callbacks: "accounts/omniauth_callbacks" }

get "admin" => "admin#index"
end
~~~

Now let's create an `AdminController`:

~~~ ruby app/controller/admin_controller.rb
class AdminController < ApplicationController
before_action :authenticate_account!

def index
end
end
~~~

And the last step would be creating a view:

~~~ html app/views/admin/index.html.erb
<h1>Admin Section</h1>

<% if current_account.authorized? %>
<% current_account.remote_rentals.each do |rental| %>
<p>
<%= rental.name %>
</p>
<% end %>
<% else %>
<%= link_to "Connect with BookingSync", account_bookingsync_omniauth_authorize_path %>
<% end %>
~~~

To make sure it's working correctly we want to display some rentals coming from BookingSync API, we will call them `remote_rentals`. We will also need to add `Account#authorized?` method for checking if this account has already been authorized to access BookingSync API.

Let's update the routes and add a controller for handling authentication process and OAuth workflow:

~~~ ruby config/routes.rb
Rails.application.routes.draw do
root to: "admin#index"

devise_for :accounts, controllers: { omniauth_callbacks: "accounts/omniauth_callbacks" }

get "admin" => "admin#index"
end
~~~

~~~ ruby app/controllers/accounts/omniauth_callbacks_controller.rb
class Accounts::OmniauthCallbacksController < Devise::OmniauthCallbacksController
before_action :authenticate_account!

def bookingsync
current_account.assign_omniauth_attributes(omniauth_attribues)

if current_account.save
set_flash_message(:notice, :success, kind: "BookingSync") if is_navigational_format?
redirect_to admin_path
else
redirect_to root_path
end
end

def failure
redirect_to root_path
end

private

def omniauth_attribues
request.env["omniauth.auth"]
end
end
~~~

Most of the logic is already handled here by `OmniAuth` and `devise`, so we don't need to do much ourselves. We just need to `Account#assign_omniauth_attributes` method to handle attributes assignment such as `uid`, `name` and token-related attributes. Here's the implementation:

~~~ app/models/account.rb
class Account < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable

devise :omniauthable, omniauth_providers: [:bookingsync]

def assign_omniauth_attributes(auth)
self.uid = auth.uid
self.provider = auth.provides
self.name = auth.info.business_name
update_token(auth.credentials)
end

def update_token(token)
self.oauth_access_token = token.token
self.oauth_refresh_token = token.refresh_token
self.oauth_expires_at = token.expires_at
end

def authorized?
oauth_access_token.present? && oauth_refresh_token.present?
end
end
~~~

We also added `Account#authorized?` method which checks if the token and refresh token are present.

That way we have all the logic required for connecting the application's account with BookingSync account. The only thing left is providing the way to fetch rentals.

## Adding bookingsync-api And Fetching Rentals

Let's add `bookingsync-api` gem which handles the communication with BookingSync API:

~~~
gem "bookingsync-api"
~~~

Now we can add some methods to `Account` model:

~~~ ruby app/models/account.rb
class Account < ApplicationRecord
# some previous methods already covered before go here

def remote_rentals
bookingsync_api_client.rentals
end

def bookingsync_api_client
@bookingsync_api_client ||= BookingSync::API.new(oauth_token)
end

def oauth_token
token.token
end

def token
@token ||= begin
token_options = {}
if oauth_refresh_token
token_options[:refresh_token] = oauth_refresh_token
token_options[:expires_at] = oauth_expires_at
end

token = OAuth2::AccessToken.new(oauth_client, oauth_access_token, token_options)

if token.expired?
refresh_token!(token)
else
token
end
end
end

def refresh_token!(current_token = token)
@token = current_token.refresh!.tap { |new_token| update_token(new_token) }
save!
@token
end

def oauth_client
client_options = {
site: "https://www.bookingsync.com",
connection_opts: { headers: { accept: "application/vnd.api+json" } }
}
client_options[:ssl] = { verify: true }
OAuth2::Client.new(ENV["BOOKINGSYNC_APPLICATION_ID"], ENV["BOOKINGSYNC_SECRET"], client_options)
end
end
~~~

`Account#remote_rentals` method is responsible for fetching `rentals` from the API and it simply delegates the logic to `bookingsync_api_client`. The API client requires `oauth_token`, which is returned in `token` method. This method also ensures that the token will also be valid - if the token expires, it will perform a proper request using `refresh_token` to get a new one. Fetching tokens requires properly configured `oauth_client` with the right headers and application's data.

And that's it! Now you can sign in, click the link to connect your account with BookingSync, perform authorization and after redirecting back to you app you should see a list of rentals (if you have created any).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

back to you app > your

2 changes: 2 additions & 0 deletions layouts/guides.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"/guides/create-a-booking-with-a-source", class: "list-group-item") %>
<%= link_to_with_current("Read all authorized accounts at once",
"/guides/read-all-authorized-accounts-at-once", class: "list-group-item") %>
<%= link_to_with_current("Set Up Rails Application With omniauth-bookingsync",
"/guides/set-up-rails-appplication-with-omniauth-bookingsync", class: "list-group-item") %>
<%= link_to_with_current("Getting Started with PHP",
"/guides/getting-started-with-php", class: "list-group-item") %>
</div>
Expand Down
Binary file added static/images/bookingsync_new_application.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.