diff --git a/.travis.yml b/.travis.yml index c801c73..cb98bd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,4 +17,7 @@ script: - RAILS_ENV=test bundle exec rake --trace db:migrate test before_script: - gem install mysql2 - - mysql -e 'create database quadbase_test' \ No newline at end of file + - mysql -e 'create database quadbase_test' +notifications: + email: + - dev@quadbase.org \ No newline at end of file diff --git a/Gemfile b/Gemfile index 928da38..7cf4455 100644 --- a/Gemfile +++ b/Gemfile @@ -47,6 +47,8 @@ gem 'therubyracer', '~> 0.10.1' gem 'bullring', '~> 0.8.3' +gem 'doorkeeper', '~> 0.6.0' + group :development, :test do gem 'sqlite3', '~> 1.3.6' gem 'debugger', '~> 1.1.4' @@ -56,6 +58,7 @@ group :development, :test do gem 'thin', '~> 1.3.1' gem 'quiet_assets', '~> 1.0.1' gem 'rvm-capistrano' + gem 'oauth2' end gem 'single_test', '~> 0.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index 30d7a2f..c36a722 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,8 @@ GEM orm_adapter (~> 0.1) railties (~> 3.1) warden (~> 1.2.1) + doorkeeper (0.6.1) + railties (~> 3.1) erubis (2.7.0) eventmachine (0.12.10) execjs (1.4.0) @@ -79,8 +81,11 @@ GEM railties (>= 3.0.0) faker (1.0.1) i18n (~> 0.4) + faraday (0.8.4) + multipart-post (~> 1.1) highline (1.6.13) hike (1.2.1) + httpauth (0.2.0) i18n (0.6.0) journey (1.0.4) jquery-rails (2.0.2) @@ -92,6 +97,8 @@ GEM jsonify-rails (0.3.2) actionpack jsonify (< 0.4.0) + jwt (0.1.5) + multi_json (>= 1.0) libv8 (3.3.10.4) mail (2.4.4) i18n (>= 0.4.0) @@ -99,6 +106,7 @@ GEM treetop (~> 1.4.8) mime-types (1.19) multi_json (1.3.6) + multipart-post (1.1.5) mysql2 (0.3.11) mysql2 (0.3.11-x86-mingw32) net-scp (1.0.4) @@ -108,6 +116,12 @@ GEM net-ssh (2.5.2) net-ssh-gateway (1.1.0) net-ssh (>= 1.99.1) + oauth2 (0.8.0) + faraday (~> 0.8) + httpauth (~> 0.1) + jwt (~> 0.1.4) + multi_json (~> 1.0) + rack (~> 1.2) orm_adapter (0.4.0) paperclip (3.0.4) activemodel (>= 3.0.0) @@ -204,6 +218,7 @@ DEPENDENCIES coffee-rails (~> 3.2.2) debugger (~> 1.1.4) devise (~> 2.1.0) + doorkeeper (~> 0.6.0) execjs (~> 1.4.0) factory_girl_rails (~> 3.4.0) faker (~> 1.0.1) @@ -212,6 +227,7 @@ DEPENDENCIES jsonify-rails (~> 0.3.2) mime-types (~> 1.18) mysql2 (~> 0.3.11) + oauth2 paperclip (~> 3.0.4) parslet (~> 1.4.0) quiet_assets (~> 1.0.1) diff --git a/README.md b/README.md index 54da78e..8334f18 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Quadbase ======== -[![Build Status](https://secure.travis-ci.org/lml/quadbase.png)](http://travis-ci.org/lml/quadbase) +[![Build Status](https://secure.travis-ci.org/lml/quadbase.png?branch=master)](http://travis-ci.org/lml/quadbase) Quadbase is an open homework and test question bank, where questions are written by the community and access is free. diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 7f0b9a8..2209cc6 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -133,3 +133,27 @@ function hide_question_until_loaded() { $(".question-wrapper").fadeIn('500'); }); } + +function open_specified_dialog(name, is_modal, height, width, title, body) { + $('#' + name + '_dialog_errors').html(''); + + $("#" + name + "_dialog_body").html(body); + + $("#" + name + "_dialog").dialog({ + autoOpen: false, + modal: is_modal, + height: height, + width: width, + title: title, + position: 'center' + }); + + refresh_buttons(); + $("#" + name + "_dialog").dialog('open'); + $("#" + name + "_dialog").scrollTop(0); + $("#" + name + "_dialog").dialog('open').closeOnClickOutside(); +} + +function open_message_dialog(is_modal, height, width, title, body) { + open_specified_dialog('message', is_modal, height, width, title, body); +} diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb new file mode 100644 index 0000000..c0a0e77 --- /dev/null +++ b/app/controllers/api/v1/api_controller.rb @@ -0,0 +1,42 @@ +module Api + module V1 + class ApiController < ApplicationController + skip_before_filter :authenticate_user! + + respond_to :json + + rescue_from Exception, :with => :rescue_from_exception + + private + + def current_user + @current_user ||= doorkeeper_token ? + User.find(doorkeeper_token.resource_owner_id) : + AnonymousUser.instance + end + + def rescue_from_exception(exception) + # See https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L453 for error names/symbols + + error = :internal_server_error + send_email = true + + case exception + when SecurityTransgression + error = :forbidden + send_email = false + when ActiveRecord::RecordNotFound, + ActionController::RoutingError, + ActionController::UnknownController, + AbstractController::ActionNotFound + error = :not_found + send_email = false + end + + DeveloperErrorNotifier.exception_email(exception, request, present_user) if send_email + head error + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/questions_controller.rb b/app/controllers/api/v1/questions_controller.rb new file mode 100644 index 0000000..08be2cb --- /dev/null +++ b/app/controllers/api/v1/questions_controller.rb @@ -0,0 +1,11 @@ +module Api + module V1 + class QuestionsController < ApiController + def show + @question = Question.from_param(params[:id]) + raise SecurityTransgression unless current_user.can_read?(@question) + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/api/v1/solutions_controller.rb b/app/controllers/api/v1/solutions_controller.rb new file mode 100644 index 0000000..6f2a87f --- /dev/null +++ b/app/controllers/api/v1/solutions_controller.rb @@ -0,0 +1,19 @@ +module Api + module V1 + class SolutionsController < ApiController + + def index + # debugger + @question = Question.from_param(params[:question_id]) + raise SecurityTransgression unless current_user.can_read?(@question) + @solutions = Vote.order_by_votes(@question.valid_solutions_visible_for(current_user)) + end + + def show + @solution = Solution.find(params[:id]) + raise SecurityTransgression unless current_user.can_read?(@solution) + end + + end + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 81f55dd..743ff62 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -16,7 +16,7 @@ class ApplicationController < ActionController::Base helper_method :user_is_disabled?, :site_in_maintenance?, - :user_is_admin?, + # :user_is_admin?, :present_user, :get_error_messages, :protect_form, @@ -24,6 +24,8 @@ class ApplicationController < ActionController::Base respond_to :html, :js + include SharedApplicationMethods + unless Quadbase::Application.config.consider_all_requests_local rescue_from Exception, :with => :rescue_from_exception end @@ -97,9 +99,9 @@ def site_not_in_maintenance! redirect_maintenance end - def user_is_admin? - user_signed_in? && current_user.is_administrator? - end + # def user_is_admin? + # user_signed_in? && current_user.is_administrator? + # end def authenticate_admin! user_is_admin? || redirect_not_admin diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 29d1826..9005d0c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -4,6 +4,9 @@ require 'digest/md5' module ApplicationHelper + + include SharedApplicationMethods + def trash_icon image_tag("trash.gif", {:border => 0, :alt => "Delete", :title => "Delete"}) #{}"".html_safe diff --git a/app/helpers/jquery_helper.rb b/app/helpers/jquery_helper.rb index 0f6542b..5bd602a 100644 --- a/app/helpers/jquery_helper.rb +++ b/app/helpers/jquery_helper.rb @@ -44,4 +44,16 @@ def reload_mathjax(element_id="") element_id + '")]);').html_safe end + def message_dialog(title=nil, options={}, &block) + specified_dialog("message", title, options, &block) + end + + def specified_dialog(name=nil, title=nil, options={}, &block) + @name ||= name + @title ||= title + @options = options + @body = capture(&block) + render :template => 'shared/specified_dialog' + end + end diff --git a/app/models/solution.rb b/app/models/solution.rb index 78186bf..4a16fc4 100644 --- a/app/models/solution.rb +++ b/app/models/solution.rb @@ -23,8 +23,8 @@ class Solution < ActiveRecord::Base scope :visible_for, lambda { |user| joins{question}.where{(creator_id == user.id) | (is_visible == true) | - ( question.version == nil && - question.id.in(Question.visible_for(user).select{id}) )} + ( (question.version == nil) & + (question.id.in(Question.visible_for(user).select{id})) )} } before_save :auto_subscribe diff --git a/app/models/user.rb b/app/models/user.rb index 4e64e1c..4498029 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,8 @@ class User < ActiveRecord::Base has_many :deputizers, :through => :received_deputizations + has_one :api_key, :dependent => :destroy + validates_presence_of :first_name, :last_name, :username, :user_profile validates_uniqueness_of :username, :case_sensitive => false validates_length_of :username, :in => 3..40 diff --git a/app/views/api/v1/multipart_questions/_show.jsonify b/app/views/api/v1/multipart_questions/_show.jsonify new file mode 100644 index 0000000..915623b --- /dev/null +++ b/app/views/api/v1/multipart_questions/_show.jsonify @@ -0,0 +1,34 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.multipart_question do + json.ingest! (render :partial => 'questions/common', + :locals => {:question => @question}) + + qs = @question.question_setup + unless qs.nil? + json.introduction do + json.markup qs.content + json.html qs.content_html + end + end + + json.parts(@question.child_question_parts) do |part| + child_question = part.child_question + + json.prerequisites(child_question.prerequisite_questions) do |pq| + json.id pq.to_param + end + + json.supported_by(child_question.supporting_questions) do |sq| + json.id sq.to_param + end + + json.ingest! (render :partial => 'simple_questions/show', + :locals => {:question => child_question, + :skip_question_setup => true, + :skip_attribution => true}) + end + + json.ingest! (render :partial => 'questions/attribution') +end diff --git a/app/views/api/v1/multipart_questions/show.jsonify b/app/views/api/v1/multipart_questions/show.jsonify new file mode 100644 index 0000000..e50a725 --- /dev/null +++ b/app/views/api/v1/multipart_questions/show.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.ingest! (render :partial => 'multipart_questions/show', + :locals => {:question => question}) diff --git a/app/views/api/v1/questions/_attribution.jsonify b/app/views/api/v1/questions/_attribution.jsonify new file mode 100644 index 0000000..c31eb51 --- /dev/null +++ b/app/views/api/v1/questions/_attribution.jsonify @@ -0,0 +1,24 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +collaborators = @question.question_collaborators + +authors = collaborators.select{|c| c.is_author} +copyright_holders = collaborators.select{|c| c.is_copyright_holder} + +json.attribution do + json.authors(authors) do |author| + json.id author.user.id + json.name author.user.full_name + end + + json.copyright_holders(copyright_holders) do |copyright_holder| + json.id copyright_holder.user.id + json.name copyright_holder.user.full_name + end + + json.license do + json.name @question.license.long_name + json.url @question.license.url + end +end \ No newline at end of file diff --git a/app/views/api/v1/questions/_common.jsonify b/app/views/api/v1/questions/_common.jsonify new file mode 100644 index 0000000..b952a13 --- /dev/null +++ b/app/views/api/v1/questions/_common.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.id question.to_param +json.url question_url(question) \ No newline at end of file diff --git a/app/views/api/v1/questions/show.jsonify b/app/views/api/v1/questions/show.jsonify new file mode 100644 index 0000000..dfdf875 --- /dev/null +++ b/app/views/api/v1/questions/show.jsonify @@ -0,0 +1,10 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +if @question.errors.none? + json.ingest! (render :partial => "#{view_dir(@question)}/show", + :locals => {:question => @question}) +else + json.ingest! (render :partial => 'shared/errors', + :locals => {:errors => @question.errors}) +end \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/_content_and_answers.jsonify b/app/views/api/v1/simple_questions/_content_and_answers.jsonify new file mode 100644 index 0000000..b21b74f --- /dev/null +++ b/app/views/api/v1/simple_questions/_content_and_answers.jsonify @@ -0,0 +1,15 @@ + +json.content do + json.markup question.content + json.html question.variated_content_html +end + +if !question.answer_choices.empty? + json.answer_choices(question.answer_choices) do |answer_choice| + json.markup answer_choice.content + json.html answer_choice.variated_content_html + json.credit answer_choice.credit + end +end + +json.answer_can_be_sketched question.answer_can_be_sketched \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/_show.jsonify b/app/views/api/v1/simple_questions/_show.jsonify new file mode 100644 index 0000000..2d0b6bb --- /dev/null +++ b/app/views/api/v1/simple_questions/_show.jsonify @@ -0,0 +1,25 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +skip_question_setup ||= false +skip_attribution ||= false + +json.simple_question do + json.ingest! (render :partial => 'questions/common', + :locals => {:question => question}) + + if !question.question_setup.blank? && !skip_question_setup + json.introduction do + json.markup question.question_setup.content + json.html question.question_setup.variated_content_html + end + end + + json.ingest! (render :partial => 'simple_questions/content_and_answers', + :locals => {:question => question}) + + if !skip_attribution + json.ingest! (render :partial => 'questions/attribution', + :locals => {:question => question}) + end +end \ No newline at end of file diff --git a/app/views/api/v1/simple_questions/show.jsonify b/app/views/api/v1/simple_questions/show.jsonify new file mode 100644 index 0000000..6b58acf --- /dev/null +++ b/app/views/api/v1/simple_questions/show.jsonify @@ -0,0 +1,5 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.ingest! (render :partial => 'simple_questions/show', + :locals => {:question => question}) diff --git a/app/views/api/v1/solutions/_show.jsonify b/app/views/api/v1/solutions/_show.jsonify new file mode 100644 index 0000000..2acefa0 --- /dev/null +++ b/app/views/api/v1/solutions/_show.jsonify @@ -0,0 +1,30 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.id solution.id +json.url solution_url(solution) +json.question_id solution.question.to_param + +json.explanation solution.explanation + +json.details do + json.markup solution.content + json.html solution.content_html +end + +json.attachments(solution.attachable_assets) do |aa| + json.name aa.local_name + json.mime_type aa.asset.attachment_content_type + json.url attachable_asset_download_url(aa) +end + +json.creator do + json.name solution.creator.full_name + json.gravatar_hash gravatar_hash(solution.creator) +end + +json.votes do + json.up solution.up_votes.count + json.down solution.down_votes.count + json.total solution.combined_vote_count +end diff --git a/app/views/api/v1/solutions/index.jsonify b/app/views/api/v1/solutions/index.jsonify new file mode 100644 index 0000000..ea8f27c --- /dev/null +++ b/app/views/api/v1/solutions/index.jsonify @@ -0,0 +1,17 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +if @question.is_multipart? + json.parts(@question.child_question_parts) do |part| + child_question = part.child_question + + json.solutions(child_question.solutions) do |solution| + json.ingest! (render :partial=>'show', :locals => {:solution => solution}) + end + end + +else + json.solutions(@solutions) do |solution| + json.ingest! (render :partial=>'show', :locals => {:solution => solution}) + end +end \ No newline at end of file diff --git a/app/views/api/v1/solutions/show.jsonify b/app/views/api/v1/solutions/show.jsonify new file mode 100644 index 0000000..744da6f --- /dev/null +++ b/app/views/api/v1/solutions/show.jsonify @@ -0,0 +1,6 @@ +# Copyright 2011-2012 Rice University. Licensed under the Affero General Public +# License version 3 or later. See the COPYRIGHT file for details. + +json.solution do + json.ingest! render(:partial => 'show', :locals => {:solution => @solution}) +end \ No newline at end of file diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 0cefcac..cc662a4 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -120,6 +120,19 @@
<%= gravatar_image current_user %>
+ +
+
API Key
+ +
+ <%= current_user.api_key.nil? ? 'None'.html_safe : current_user.api_key.access_token %> + <%= link_to "Regenerate", new_user_api_key_path(current_user), :remote => true, :style => 'float:right' %> +
+ +

<%= link_to_help 'api_keys', "Learn more about Quadbase API keys" %>

+ +
+ diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 0000000..0ab352d --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,34 @@ +<%= form_for([:oauth, application]) do |f| %> +
+ <% if application.errors.any? %> +
×

Whoops! Check your form for possible errors

+ <% end %> + +
+ <%= f.label :name %> +
+ <%= f.text_field :name %> + <%= errors_for application, :name %> +
+
+ +
+ <%= f.label :redirect_uri %> +
+ <%= f.text_field :redirect_uri %> + <%= errors_for application, :redirect_uri %> + <% if Doorkeeper.configuration.test_redirect_uri %> + Use <%= Doorkeeper.configuration.test_redirect_uri %> for local tests + <% end %> +
+
+ +
+ +
+ <%= f.submit :Submit, :class => "btn primary" %> + <%= link_to "Cancel", oauth_applications_path, :class => "btn" %> +
+
+<% end %> + diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 0000000..7df339c --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,13 @@ +
+ +
+ +
+ <%= render 'form', :application => @application %> +
+ +
+

Actions

+

<%= link_to 'Back to application list', oauth_applications_path %>

+
+ diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 0000000..2488a6b --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,25 @@ +<%= pageHeading "Oauth Applications" %> + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + + <% end %> + +
NameCallback url
<%= link_to application.name, [:oauth, application] %><%= application.redirect_uri %><%= link_to 'Edit', edit_oauth_application_path(application) %><%= link_to 'Destroy', [:oauth, application], :data => { :confirm => 'Are you sure?' }, :method => :delete %>
+ + +

<%= link_to 'New Application', new_oauth_application_path %>

diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 0000000..0f82126 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,13 @@ +
+ +
+ +
+ <%= render 'form', :application => @application %> +
+ +
+

Actions

+

<%= link_to 'Back to application list', oauth_applications_path %>

+
+ diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 0000000..56bef81 --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,26 @@ +
+ +
+ +
+

Callback url:

+

<%= @application.redirect_uri %>

+ +

Application Id:

+

<%= @application.uid %>

+ +

Secret:

+

<%= @application.secret %>

+ +

Link to authorization code:

+

<%= link_to 'Authorize', oauth_authorization_path(:client_id => @application.uid, :redirect_uri => @application.redirect_uri, :response_type => 'code' ) %>

+
+ +
+

Actions

+

<%= link_to 'List all', oauth_applications_path %>

+

<%= link_to 'Edit', edit_oauth_application_path(@application) %>

+

<%= link_to 'Remove', [:oauth, @application], :method => :delete, :data => { :confirm => "Are you sure?" } %>

+
diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 0000000..c3dd1f6 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,6 @@ +
+

An error has occurred

+

+

<%= @pre_auth.error_response.body[:error_description] %>
+

+
diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 0000000..30a2000 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,37 @@ +
+

Authorize <%= @pre_auth.client.name %> to use your account?

+
+ +
+ <% if @pre_auth.scopes %> +

+ This application will be able to: +

+ + <% end %> + +
+ <%= form_tag oauth_authorization_path, :method => :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag "Authorize", :class => "btn success" %> or + <% end %> +
+
+ <%= form_tag oauth_authorization_path, :method => :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag "Deny", :class => "btn" %> + <% end %> +
+
diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 0000000..df03b44 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,4 @@ +
+

Authorization code:

+ <%= params[:code] %> +
diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 0000000..9016dea --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,25 @@ +
+ + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
ApplicationAuthorized at
<%= application.name %><%= application.created_at %><%= link_to 'Revoke', oauth_authorized_application_path(application), :data => { :confirm => 'Are you sure?' }, :method => :delete, :class => 'btn danger' %>
+
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 @@ +
    +
    +
    + <% unless signed_in? %> +

    Sign in first to explore the provider's API

    +

    Sign in with OAuth 2 provider »

    + <% end %> +
    +
    +
    + +
    +
    +
    + <% if signed_in? %> +

    Explore the API

    + + + +

    Your access token: <%= session[:access_token] %> (refresh)

    + +

    Select one of the api methods above

    + <% end %> +
    +
    +
    diff --git a/examples/oauth-client/views/layout.erb b/examples/oauth-client/views/layout.erb new file mode 100644 index 0000000..6cfa8d2 --- /dev/null +++ b/examples/oauth-client/views/layout.erb @@ -0,0 +1,41 @@ + + + + Example Quadbase OAuth 2 Client + + + + + +
    +
    +
    + Example Quadbase OAuth 2 Client + +
    +
    + + + +
    + <%= yield %> +
    + + + + + + diff --git a/lib/api_constraints.rb b/lib/api_constraints.rb new file mode 100644 index 0000000..8693c3c --- /dev/null +++ b/lib/api_constraints.rb @@ -0,0 +1,10 @@ +class ApiConstraints + def initialize(options) + @version = options[:version] + @default = options[:default] + end + + def matches?(req) + @default || req.headers['Accept'].include?("application/vnd.quadbase.v#{@version}") + end +end \ No newline at end of file diff --git a/lib/shared_application_methods.rb b/lib/shared_application_methods.rb new file mode 100644 index 0000000..49eac7c --- /dev/null +++ b/lib/shared_application_methods.rb @@ -0,0 +1,13 @@ +# We originally had some methods in the ApplicationController that were shared +# with views using helper_method. However, when we added some isolate engines +# (e.g. Doorkeeper), they were not able to access these methods from the views +# (from the layouts). So we put them here and explicitly include then in both +# the ApplicationController and ApplicationHelper + +module SharedApplicationMethods + + def user_is_admin? + user_signed_in? && current_user.is_administrator? + end + +end \ No newline at end of file diff --git a/test/factories.rb b/test/factories.rb index 21c611b..b7cb110 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -126,7 +126,6 @@ def make_list(options = {}) factory :simple_question do |f| f.content { FactoryGirl.generate(:couple_of_words) } f.association :question_setup - f.number { FactoryGirl.generate(:unique_number) } f.license_id { common_license.id } f.version nil end @@ -281,4 +280,9 @@ def make_list(options = {}) f.variables "x" end + factory :oauth_application, class: Doorkeeper::Application do |f| + f.name { FactoryGirl.generate(:couple_of_words) } + f.redirect_uri 'http://localhost:3000' + end + end diff --git a/test/integration/api/common_test.rb b/test/integration/api/common_test.rb new file mode 100644 index 0000000..04b6dec --- /dev/null +++ b/test/integration/api/common_test.rb @@ -0,0 +1,20 @@ +require 'integration/api/integration_test' + +class Api::CommonTest < Api::IntegrationTest + + setup do + @oauth_application = FactoryGirl.create(:oauth_application) + end + + test "tokens never expire" do + user = FactoryGirl.create(:user, :password => 'password') + token = oauth_token_wrapper(@oauth_application, user.email, "password") + assert_nil token.oauth_token.expires_at + Timecop.travel(Time.local(2999,1,1,1,0,0)) + assert !token.oauth_token.expired? + Timecop.return + end + +end + + diff --git a/test/integration/api/integration_test.rb b/test/integration/api/integration_test.rb new file mode 100644 index 0000000..a41f7a9 --- /dev/null +++ b/test/integration/api/integration_test.rb @@ -0,0 +1,58 @@ +require 'test_helper' +require 'json' + +# References: +# http://twobitlabs.com/2010/09/setting-request-headers-in-rails-functional-tests/ + +class Api::IntegrationTest < ActionDispatch::IntegrationTest + + class TokenWrapper + attr_reader :oauth_token + + def initialize(oauth_token) + @oauth_token = oauth_token + end + + def get(url, api_version, params={}) + @oauth_token.get(url, {:headers => {'Accept' => "application/vnd.quadbase.#{api_version}"}}) + end + end + + def oauth_token_wrapper(application, email, password) + client = OAuth2::Client.new(application.uid, application.secret) do |b| + b.request :url_encoded + b.adapter :rack, Rails.application + end + TokenWrapper.new(client.password.get_token(email, password)) + end + + def api_call(method, url, api_version, params={}) + case method + when :get + get(url, params, {'Accept' => "application/vnd.quadbase.#{api_version}"}) + end + end + + def assert_oauth_error(expected_status, msg=nil) + got_oauth_error = false + oauth_error_status = expected_status + + begin + yield + rescue OAuth2::Error => e + got_oauth_error = true + oauth_error_status = e.response.status + end + + if !got_oauth_error + flunk(build_message(msg, "Expression did not produce an oauth error as expected")) + elsif oauth_error_status != expected_status + flunk(build_message(msg, + "Expression expected to produce an oauth error with status ? but had status ?", + expected_status, oauth_error_status)) + end + end + +end + + diff --git a/test/integration/api/v1/questions_controller_test.rb b/test/integration/api/v1/questions_controller_test.rb new file mode 100644 index 0000000..99d944a --- /dev/null +++ b/test/integration/api/v1/questions_controller_test.rb @@ -0,0 +1,51 @@ +require 'integration/api/integration_test' + +class Api::V1::QuestionsControllerTest < Api::IntegrationTest + + setup do + @published_question = make_simple_question(:method => :create, :published => true) + @unpublished_question = make_simple_question(:method => :create) + @oauth_application = FactoryGirl.create(:oauth_application) + + @unpublished_question_user = @unpublished_question.question_collaborators.first.user + @unpublished_question_user.update_attribute(:password, "password") + + @published_question_user = @published_question.question_collaborators.first.user + @published_question_user.update_attribute(:password, "password") + end + + test "should be able to access published question without credentials" do + response = api_call :get, "/api/questions/#{@published_question.to_param}", "v1" + assert_response :success + json = JSON.parse(@response.body) + # lame test that the api is working; ponder switch to rspec and https://github.com/collectiveidea/json_spec + assert json.has_key?("simple_question") + end + + test "should get published question using non author oauth credentials" do + token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") + response = token.get("/api/questions/#{@published_question.to_param}", "v1") + assert_equal 200, response.status + end + + test "should not be able to access unpublished question without credentials" do + api_call :get, "/api/questions/#{@unpublished_question.to_param}", "v1" + assert_response :forbidden + end + + test "should get unpublished question using oauth credentials" do + token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") + response = token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + assert_equal 200, response.status + end + + test "should not get unpublished question using wrong oauth credentials" do + token = oauth_token_wrapper(@oauth_application, @published_question_user.email, "password") + assert_oauth_error (403) { + token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + } + end + +end + + diff --git a/test/integration/api/v1/solutions_controller_test.rb b/test/integration/api/v1/solutions_controller_test.rb new file mode 100644 index 0000000..857feb9 --- /dev/null +++ b/test/integration/api/v1/solutions_controller_test.rb @@ -0,0 +1,65 @@ +require 'integration/api/integration_test' + +class Api::V1::SolutionsControllerTest < Api::IntegrationTest + + setup do + @oauth_application = FactoryGirl.create(:oauth_application) + + ContentParseAndCache.enable_test_parser = true + @user = FactoryGirl.create(:user, :password => "password") + @solution = FactoryGirl.create(:solution, :creator => @user) + @question = @solution.question + + @published_question = make_simple_question(:method => :create, :published => true) + @published_question_user = @published_question.question_collaborators.first.user + @published_question_user.update_attribute(:password, "password") + @published_solution = FactoryGirl.create(:solution, :question => @published_question) + @visible_published_solution = FactoryGirl.create(:solution, :question => @published_question, + :is_visible => true) + ContentParseAndCache.enable_test_parser = false + end + + test "should be able to list published question solutions without credentials" do + response = api_call :get, "/api/questions/#{@published_question.to_param}/solutions", "v1" + assert_response :success + json = JSON.parse(@response.body) + # jj json + assert_equal json["solutions"].length, 1 + end + + test "should get single visible solution for published question without credentials" do + api_call :get, "/api/solutions/#{@visible_published_solution.id}", "v1" + assert_response :success + end + + test "should get published question solutions using non author oauth credentials" do + token = oauth_token_wrapper(@oauth_application, @user.email, "password") + response = token.get("/api/questions/#{@published_question.to_param}/solutions", "v1") + # puts JSON.parse(response.body) + assert_equal 200, response.status + end + + # test "should not be able to access unpublished question without credentials" do + # api_call :get, "/api/questions/#{@unpublished_question.to_param}", "v1" + # assert_response :forbidden + # end + + # test "should get unpublished question using oauth credentials" do + # token = oauth_token_wrapper(@oauth_application, @unpublished_question_user.email, "password") + # response = token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + # assert_equal 200, response.status + # end + + # test "should not get unpublished question using wrong oauth credentials" do + # token = oauth_token_wrapper(@oauth_application, @published_question_user.email, "password") + # assert_oauth_error (403) { + # token.get("/api/questions/#{@unpublished_question.to_param}", "v1") + # } + # end + +end + + + + +