diff --git a/app/views/help/topics/_api_keys.html.erb b/app/views/help/topics/_api_keys.html.erb
new file mode 100644
index 0000000..c206234
--- /dev/null
+++ b/app/views/help/topics/_api_keys.html.erb
@@ -0,0 +1,3 @@
+
Your API key is used to access Quadbase from other systems. Protect your API key like a password.
+You can get a new API key at any time by clicking the "regenerate" button. Note that this will permanently
+delete any existing API key you have.
\ No newline at end of file
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index b3994ea..e1586a0 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,6 +1,8 @@
<%# Copyright 2011-2012 Rice University. Licensed under the Affero General Public
License version 3 or later. See the COPYRIGHT file for details. %>
+
+
@@ -142,7 +144,7 @@
<% end %>
- <% if user_is_admin? %>
+ <% if user_is_admin? %>
>
<%= link_to "Admin", admin_path %>
@@ -205,6 +207,11 @@
<% if @include_help_dialog %>
<% end %>
+
+
+
+
+
<%= yield :javascript %>
diff --git a/app/views/shared/specified_dialog.js.erb b/app/views/shared/specified_dialog.js.erb
new file mode 100644
index 0000000..c01a6a3
--- /dev/null
+++ b/app/views/shared/specified_dialog.js.erb
@@ -0,0 +1,15 @@
+<%# Copyright 2011-2012 Rice University. Licensed under the Affero General Public
+ License version 3 or later. See the COPYRIGHT file for details. %>
+
+<%
+ @options ||= {}
+ @height = @options[:height] || 300
+ @width = @options[:width] || 400
+ @body ||= yield #'Specify a body argument!'
+ @title ||= ''
+%>
+
+open_specified_dialog('<%= @name %>', true, <%= @height %>, <%= @width %>, "<%= @title %>", "<%= escape_javascript(@body) %>");
+
+refresh_buttons();
+<%= reload_mathjax("#{@name}_dialog") %>
diff --git a/config/application.rb b/config/application.rb
index a6dc708..29645f5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -83,6 +83,10 @@ class Application < Rails::Application
# Example:
# config.question_lock_timeout = 5.minutes
config.question_lock_timeout = 0
+
+ config.to_prepare do
+ Doorkeeper::ApplicationController.layout "application"
+ end
end
end
diff --git a/config/environment.rb b/config/environment.rb
index 8a8718f..ad28621 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -4,6 +4,7 @@
# Load the rails application
require File.expand_path('../application', __FILE__)
+require 'shared_application_methods'
require 'extensions'
require 'uri_validator'
require 'quadbase_markup'
@@ -15,6 +16,7 @@
require 'acts_as_numberable'
require 'variated_content_html'
require 'will_paginate/array'
+require 'api_constraints'
ActionMailer::Base.delivery_method = :sendmail
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 8ce8cf5..1e821f1 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -70,7 +70,7 @@
# Took ":string => true" out b/c session cookies weren't making it across the break
# from https to http
- config.middleware.use Rack::SslEnforcer, :only => /^\/users/ #, :strict => true
+ config.middleware.use Rack::SslEnforcer, :only => /^\/users|^\/api/ #, :strict => true
#config.middleware.insert_before ActionDispatch::Cookies, Rack::SslEnforcer
# Log the query plan for queries taking more than this (works
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
new file mode 100644
index 0000000..e8da945
--- /dev/null
+++ b/config/initializers/doorkeeper.rb
@@ -0,0 +1,67 @@
+Doorkeeper.configure do
+ # Change the ORM that doorkeeper will use.
+ # Currently supported options are :active_record, :mongoid2, :mongoid3, :mongo_mapper
+ orm :active_record
+
+ # This block will be called to check whether the resource owner is authenticated or not.
+ resource_owner_authenticator do
+ # raise "Please configure doorkeeper resource_owner_authenticator block located in #{__FILE__}"
+ # Put your resource owner authentication logic here.
+ current_user || warden.authenticate!(:scope => :user)
+ # Example implementation:
+ # User.find_by_id(session[:user_id]) || redirect_to(new_user_session_url)
+ end
+
+ # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
+ admin_authenticator do
+ current_user && current_user.is_administrator? || warden.authenticate!(:scope => :user)
+ end
+
+ # Authorization Code expiration time (default 10 minutes).
+ # authorization_code_expires_in 10.minutes
+
+ # Access token expiration time (default 2 hours).
+ # If you want to disable expiration, set this to nil.
+ access_token_expires_in nil
+
+ # Issue access tokens with refresh token (disabled by default)
+ # use_refresh_token
+
+ # Provide support for an owner to be assigned to each registered application (disabled by default)
+ # Optional parameter :confirmation => true (default false) if you want to enforce ownership of
+ # a registered application
+ # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support
+ # enable_application_owner :confirmation => false
+
+ # Define access token scopes for your provider
+ # For more information go to https://github.com/applicake/doorkeeper/wiki/Using-Scopes
+ # default_scopes :public
+ # optional_scopes :write, :update
+
+ # Change the way client credentials are retrieved from the request object.
+ # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
+ # falls back to the `:client_id` and `:client_secret` params from the `params` object.
+ # Check out the wiki for more information on customization
+ # client_credentials :from_basic, :from_params
+
+ # Change the way access token is authenticated from the request object.
+ # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
+ # falls back to the `:access_token` or `:bearer_token` params from the `params` object.
+ # Check out the wiki for mor information on customization
+ # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param
+
+ # Change the test redirect uri for client apps
+ # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider
+ # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL
+ # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi)
+ #
+ # test_redirect_uri 'urn:ietf:wg:oauth:2.0:oob'
+
+ # Enable password authentication in the test environment
+ if Rails.env.test?
+ resource_owner_from_credentials do |routes|
+ u = User.find_for_database_authentication(:email => params[:username])
+ u if u && u.valid_password?(params[:password])
+ end
+ end
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
new file mode 100644
index 0000000..8a51ce2
--- /dev/null
+++ b/config/locales/doorkeeper.en.yml
@@ -0,0 +1,68 @@
+en:
+ activerecord:
+ errors:
+ models:
+ application:
+ attributes:
+ redirect_uri:
+ fragment_present: 'cannot contain a fragment.'
+ has_query_parameter: 'cannot contain a query parameter.'
+ invalid_uri: 'must be a valid URI.'
+ relative_uri: 'must be an absolute URI.'
+ mongoid:
+ errors:
+ models:
+ application:
+ attributes:
+ redirect_uri:
+ fragment_present: 'cannot contain a fragment.'
+ has_query_parameter: 'cannot contain a query parameter.'
+ invalid_uri: 'must be a valid URI.'
+ relative_uri: 'must be an absolute URI.'
+ mongo_mapper:
+ errors:
+ models:
+ application:
+ attributes:
+ redirect_uri:
+ fragment_present: 'cannot contain a fragment.'
+ has_query_parameter: 'cannot contain a query parameter.'
+ invalid_uri: 'must be a valid URI.'
+ relative_uri: 'must be an absolute URI.'
+ doorkeeper:
+ errors:
+ messages:
+ # Common error messages
+ invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.'
+ invalid_redirect_uri: 'The redirect uri included is not valid.'
+ unauthorized_client: 'The client is not authorized to perform this request using this method.'
+ access_denied: 'The resource owner or authorization server denied the request.'
+ invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
+ server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
+ temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
+
+ #configuration error messages
+ credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.'
+ resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.'
+
+ # Access grant errors
+ unsupported_response_type: 'The authorization server does not support this response type.'
+
+ # Access token errors
+ invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.'
+ invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.'
+ unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.'
+
+ # Password Access token errors
+ invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found'
+ flash:
+ applications:
+ create:
+ notice: 'Application created.'
+ destroy:
+ notice: 'Application deleted.'
+ update:
+ notice: 'Application updated.'
+ authorized_applications:
+ destroy:
+ notice: 'Application revoked.'
diff --git a/config/routes.rb b/config/routes.rb
index 22d3a03..12012e3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -3,6 +3,18 @@
Quadbase::Application.routes.draw do
+ use_doorkeeper
+
+ namespace :api, defaults: {format: 'json'} do
+ scope module: :v1, constraints: ApiConstraints.new(version: 1) do
+ resources :questions, :only => [:show] do
+ resources :solutions, :only => [:index]
+ end
+ resources :solutions, :only => [:show]
+ end
+ end
+
+
namespace :admin do
resources :logic_libraries do
resources :logic_library_versions, :shallow => true
@@ -73,6 +85,7 @@ def votable
resources :users, :only => [:index, :show, :edit, :update] do
post 'become'
post 'confirm'
+ resources :api_keys, :shallow => true, :only => [:new, :create]
end
resources :deputizations, :only => [:create, :destroy, :new] do
diff --git a/db/migrate/20121109221727_create_doorkeeper_tables.rb b/db/migrate/20121109221727_create_doorkeeper_tables.rb
new file mode 100644
index 0000000..26c2616
--- /dev/null
+++ b/db/migrate/20121109221727_create_doorkeeper_tables.rb
@@ -0,0 +1,45 @@
+class CreateDoorkeeperTables < ActiveRecord::Migration
+ def change
+ create_table :oauth_applications do |t|
+ t.string :name, :null => false
+ t.string :uid, :null => false
+ t.string :secret, :null => false
+ t.string :redirect_uri, :null => false
+ t.integer :owner_id, :null => true
+ t.string :owner_type, :null => true
+ t.timestamps
+ end
+
+ add_index :oauth_applications, :uid, :unique => true
+ add_index :oauth_applications, [:owner_id, :owner_type]
+
+ create_table :oauth_access_grants do |t|
+ t.integer :resource_owner_id, :null => false
+ t.integer :application_id, :null => false
+ t.string :token, :null => false
+ t.integer :expires_in, :null => false
+ t.string :redirect_uri, :null => false
+ t.datetime :created_at, :null => false
+ t.datetime :revoked_at
+ t.string :scopes
+ end
+
+ add_index :oauth_access_grants, :token, :unique => true
+
+ create_table :oauth_access_tokens do |t|
+ t.integer :resource_owner_id
+ t.integer :application_id, :null => false
+ t.string :token, :null => false
+ t.string :refresh_token
+ t.integer :expires_in
+ t.datetime :revoked_at
+ t.datetime :created_at, :null => false
+ t.string :scopes
+ end
+
+ add_index :oauth_access_tokens, :token, :unique => true
+ add_index :oauth_access_tokens, :resource_owner_id
+ add_index :oauth_access_tokens, :refresh_token, :unique => true
+
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7bf1275..c69ba6e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20120726211039) do
+ActiveRecord::Schema.define(:version => 20121109221727) do
create_table "announcements", :force => true do |t|
t.integer "user_id"
@@ -35,6 +35,13 @@
add_index "answer_choices", ["question_id"], :name => "index_answer_choices_on_question_id"
+ create_table "api_keys", :force => true do |t|
+ t.string "access_token"
+ t.integer "user_id"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
create_table "assets", :force => true do |t|
t.string "attachment_file_name"
t.string "attachment_content_type"
@@ -186,6 +193,48 @@
t.datetime "updated_at", :null => false
end
+ create_table "oauth_access_grants", :force => true do |t|
+ t.integer "resource_owner_id", :null => false
+ t.integer "application_id", :null => false
+ t.string "token", :null => false
+ t.integer "expires_in", :null => false
+ t.string "redirect_uri", :null => false
+ t.datetime "created_at", :null => false
+ t.datetime "revoked_at"
+ t.string "scopes"
+ end
+
+ add_index "oauth_access_grants", ["token"], :name => "index_oauth_access_grants_on_token", :unique => true
+
+ create_table "oauth_access_tokens", :force => true do |t|
+ t.integer "resource_owner_id"
+ t.integer "application_id", :null => false
+ t.string "token", :null => false
+ t.string "refresh_token"
+ t.integer "expires_in"
+ t.datetime "revoked_at"
+ t.datetime "created_at", :null => false
+ t.string "scopes"
+ end
+
+ add_index "oauth_access_tokens", ["refresh_token"], :name => "index_oauth_access_tokens_on_refresh_token", :unique => true
+ add_index "oauth_access_tokens", ["resource_owner_id"], :name => "index_oauth_access_tokens_on_resource_owner_id"
+ add_index "oauth_access_tokens", ["token"], :name => "index_oauth_access_tokens_on_token", :unique => true
+
+ create_table "oauth_applications", :force => true do |t|
+ t.string "name", :null => false
+ t.string "uid", :null => false
+ t.string "secret", :null => false
+ t.string "redirect_uri", :null => false
+ t.integer "owner_id"
+ t.string "owner_type"
+ t.datetime "created_at", :null => false
+ t.datetime "updated_at", :null => false
+ end
+
+ add_index "oauth_applications", ["owner_id", "owner_type"], :name => "index_oauth_applications_on_owner_id_and_owner_type"
+ add_index "oauth_applications", ["uid"], :name => "index_oauth_applications_on_uid", :unique => true
+
create_table "question_collaborators", :force => true do |t|
t.integer "user_id"
t.integer "question_id"
diff --git a/examples/oauth-client/.gitignore b/examples/oauth-client/.gitignore
new file mode 100644
index 0000000..1327e51
--- /dev/null
+++ b/examples/oauth-client/.gitignore
@@ -0,0 +1 @@
+env.rb
diff --git a/examples/oauth-client/.rvmrc b/examples/oauth-client/.rvmrc
new file mode 100644
index 0000000..21edc04
--- /dev/null
+++ b/examples/oauth-client/.rvmrc
@@ -0,0 +1 @@
+rvm --create ruby-1.9.3-p194@quadbase-ouath-client
diff --git a/examples/oauth-client/Gemfile b/examples/oauth-client/Gemfile
new file mode 100644
index 0000000..cf64e19
--- /dev/null
+++ b/examples/oauth-client/Gemfile
@@ -0,0 +1,6 @@
+source "http://rubygems.org"
+
+gem 'sinatra'
+gem 'oauth2'
+gem 'pry'
+gem 'redcarpet'
diff --git a/examples/oauth-client/Gemfile.lock b/examples/oauth-client/Gemfile.lock
new file mode 100644
index 0000000..91a9d71
--- /dev/null
+++ b/examples/oauth-client/Gemfile.lock
@@ -0,0 +1,43 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ addressable (2.2.6)
+ coderay (0.9.8)
+ faraday (0.7.5)
+ addressable (~> 2.2.6)
+ multipart-post (~> 1.1.3)
+ rack (>= 1.1.0, < 2)
+ method_source (0.6.7)
+ ruby_parser (>= 2.3.1)
+ multi_json (1.0.4)
+ multipart-post (1.1.4)
+ oauth2 (0.5.1)
+ faraday (~> 0.7.4)
+ multi_json (~> 1.0.3)
+ pry (0.9.7.4)
+ coderay (~> 0.9.8)
+ method_source (~> 0.6.7)
+ ruby_parser (>= 2.3.1)
+ slop (~> 2.1.0)
+ rack (1.3.5)
+ rack-protection (1.1.4)
+ rack
+ redcarpet (2.0.0b5)
+ ruby_parser (2.3.1)
+ sexp_processor (~> 3.0)
+ sexp_processor (3.0.8)
+ sinatra (1.3.1)
+ rack (~> 1.3, >= 1.3.4)
+ rack-protection (~> 1.1, >= 1.1.2)
+ tilt (~> 1.3, >= 1.3.3)
+ slop (2.1.0)
+ tilt (1.3.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ oauth2
+ pry
+ redcarpet
+ sinatra
diff --git a/examples/oauth-client/README.md b/examples/oauth-client/README.md
new file mode 100644
index 0000000..5fc23b5
--- /dev/null
+++ b/examples/oauth-client/README.md
@@ -0,0 +1,17 @@
+# Example Quadbase OAuth 2 Client
+
+This app is an example of OAuth 2 client, based on the DoorKeeper [example client](https://github.com/applicake/doorkeeper-sinatra-client).
+
+## Installation
+
+Here are the steps for firing up this client app.
+
+1. Run ````bundle install````. This example directory has its own ````.rvmrc```` file for setting up an RVM gem dir, so running ````bundle```` will not interfere with your Quadbase gem dir.
+2. Create a [new oauth app](http://localhost:3000/oauth/applications/new) in your development instance of Quadbase.
+3. Create an ````env.rb```` file in the top-level ````oauth-client```` directory that has the following contents, where the ````OAUTH2_CLIENT_ID```` and ````OAUTH2_CLIENT_SECRET```` have the appropriate values from the result of the prior step.
+
+ # Change these hashes to match what your local version of Quadbase gives you
+ ENV['OAUTH2_CLIENT_ID'] = "40348dc38..."
+ ENV['OAUTH2_CLIENT_SECRET'] = "69d7e8493..."
+ ENV['OAUTH2_CLIENT_REDIRECT_URI'] = "http://localhost:9292/callback"
+4. Run ````rackup config.ru```` to start the server on port 9292.
diff --git a/examples/oauth-client/config.ru b/examples/oauth-client/config.ru
new file mode 100644
index 0000000..338c6ac
--- /dev/null
+++ b/examples/oauth-client/config.ru
@@ -0,0 +1,8 @@
+require 'rubygems'
+require 'bundler'
+
+Bundler.require
+
+require './quadbase_client'
+
+run QuadbaseClient
diff --git a/examples/oauth-client/public/application.css b/examples/oauth-client/public/application.css
new file mode 100644
index 0000000..6021ba3
--- /dev/null
+++ b/examples/oauth-client/public/application.css
@@ -0,0 +1,19 @@
+body {
+ padding-top: 60px;
+}
+
+section > .row {
+ margin-bottom: 10px;
+}
+
+.row h2 {
+ margin-bottom: 10px;
+}
+
+.row h3 {
+ margin-bottom: 7px;
+}
+
+pre > code {
+ background-color: transparent;
+}
diff --git a/examples/oauth-client/public/application.js b/examples/oauth-client/public/application.js
new file mode 100644
index 0000000..6e8892d
--- /dev/null
+++ b/examples/oauth-client/public/application.js
@@ -0,0 +1,32 @@
+$(function() {
+ $('[data-explore]').click(function() {
+ var link = $(this);
+ link.button('loading');
+ $('#display-json').load(link.attr('href'), function() {
+ link.button('reset');
+ });
+ return false;
+ });
+});
+
+function syntaxHighlight(json) {
+ if (typeof json != 'string') {
+ json = JSON.stringify(json, undefined, 2);
+ }
+ json = json.replace(/&/g, '&').replace(//g, '>');
+ return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
+ var cls = 'number';
+ if (/^"/.test(match)) {
+ if (/:$/.test(match)) {
+ cls = 'key';
+ } else {
+ cls = 'string';
+ }
+ } else if (/true|false/.test(match)) {
+ cls = 'boolean';
+ } else if (/null/.test(match)) {
+ cls = 'null';
+ }
+ return '' + match + '';
+ });
+}
diff --git a/examples/oauth-client/quadbase_client.rb b/examples/oauth-client/quadbase_client.rb
new file mode 100644
index 0000000..2aea696
--- /dev/null
+++ b/examples/oauth-client/quadbase_client.rb
@@ -0,0 +1,96 @@
+require "sinatra/base"
+require 'logger'
+
+# Load custom environment variables
+load 'env.rb' if File.exists?('env.rb')
+
+class QuadbaseClient < Sinatra::Base
+ enable :sessions
+
+ helpers do
+ include Rack::Utils
+ alias_method :h, :escape_html
+
+ def pretty_json(json)
+ JSON.pretty_generate(json)
+ end
+
+ def signed_in?
+ !session[:access_token].nil?
+ end
+ end
+
+ logger = Logger.new(STDOUT)
+
+ def client(token_method = :post)
+ OAuth2::Client.new(
+ ENV['OAUTH2_CLIENT_ID'],
+ ENV['OAUTH2_CLIENT_SECRET'],
+ :site => ENV['SITE'] || "http://localhost:3000",
+ :token_method => token_method,
+ )
+ end
+
+ def access_token
+ OAuth2::AccessToken.new(client, session[:access_token], :refresh_token => session[:refresh_token])
+ end
+
+ def redirect_uri
+ ENV['OAUTH2_CLIENT_REDIRECT_URI']
+ end
+
+ get '/' do
+ erb :home
+ end
+
+ get '/sign_in' do
+ # scope = params[:scope] || "public"
+ # redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri, :scope => scope)
+ redirect client.auth_code.authorize_url(:redirect_uri => redirect_uri)
+ end
+
+ get '/sign_out' do
+ session[:access_token] = nil
+ redirect '/'
+ end
+
+ get '/callback' do
+ new_token = client.auth_code.get_token(params[:code], :redirect_uri => redirect_uri)
+ session[:access_token] = new_token.token
+ session[:refresh_token] = new_token.refresh_token
+ redirect '/'
+ end
+
+ get '/refresh' do
+ new_token = access_token.refresh!
+ session[:access_token] = new_token.token
+ session[:refresh_token] = new_token.refresh_token
+ redirect '/'
+ end
+
+ # get '/explore/:api' do
+ # raise "Please call a valid endpoint" unless params[:api]
+ # begin
+ # response = access_token.get("/api/#{params[:api]}/d1", {:headers => {'Accept' => 'application/vnd.quadbase.v1'}})
+ # @json = JSON.parse(response.body)
+ # erb :explore, :layout => !request.xhr?
+ # rescue OAuth2::Error => @error
+ # erb :error, :layout => !request.xhr?
+ # end
+ # end
+
+ get '/explore/*' do
+
+ @endpoint = params[:splat].first
+
+ raise "Please call a valid endpoint" unless @endpoint
+ begin
+ response = access_token.get("/api/#{@endpoint}", {:headers => {'Accept' => 'application/vnd.quadbase.v1'}})
+ @json = JSON.parse(response.body)
+ erb :explore, :layout => !request.xhr?
+ rescue OAuth2::Error => @error
+ erb :error, :layout => !request.xhr?
+ end
+ end
+
+end
diff --git a/examples/oauth-client/views/error.erb b/examples/oauth-client/views/error.erb
new file mode 100644
index 0000000..cc23bdc
--- /dev/null
+++ b/examples/oauth-client/views/error.erb
@@ -0,0 +1,2 @@
+
OAuth2::Error
+<%= h @error.response.inspect %>
diff --git a/examples/oauth-client/views/explore.erb b/examples/oauth-client/views/explore.erb
new file mode 100644
index 0000000..58df5da
--- /dev/null
+++ b/examples/oauth-client/views/explore.erb
@@ -0,0 +1,19 @@
+
+
+
API endpoint /api/<%= @endpoint %> returns...
+
+
+
+
\ No newline at end of file
diff --git a/examples/oauth-client/views/home.erb b/examples/oauth-client/views/home.erb
new file mode 100644
index 0000000..d3ab894
--- /dev/null
+++ b/examples/oauth-client/views/home.erb
@@ -0,0 +1,30 @@
+
+