diff --git a/content/guides/set-up-rails-appplication-with-omniauth-bookingsync.md b/content/guides/set-up-rails-appplication-with-omniauth-bookingsync.md new file mode 100644 index 00000000..45d482da --- /dev/null +++ b/content/guides/set-up-rails-appplication-with-omniauth-bookingsync.md @@ -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) %>
+ <%- end -%> +<% end -%> +~~~ + +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: + +
+ BookingSync +
+ +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. + +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). + +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). + +~~~ +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 +

Admin Section

+ +<% if current_account.authorized? %> + <% current_account.remote_rentals.each do |rental| %> +

+ <%= rental.name %> +

+ <% 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). diff --git a/layouts/guides.html b/layouts/guides.html index 9cd76174..b4019be7 100644 --- a/layouts/guides.html +++ b/layouts/guides.html @@ -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") %> diff --git a/static/images/bookingsync_new_application.png b/static/images/bookingsync_new_application.png new file mode 100644 index 00000000..f33bb836 Binary files /dev/null and b/static/images/bookingsync_new_application.png differ