Merge branch 'sf-use-services'
Conflicts: Gemfile.lock lib/travis/api/app/endpoint/authorization.rb
This commit is contained in:
commit
9e98c3b1f0
3
Gemfile
3
Gemfile
|
@ -4,10 +4,11 @@ source :rubygems
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
gem 'travis-support', github: 'travis-ci/travis-support'
|
gem 'travis-support', github: 'travis-ci/travis-support'
|
||||||
gem 'travis-core', github: 'travis-ci/travis-core'
|
gem 'travis-core', github: 'travis-ci/travis-core', branch: 'sf-more-services'
|
||||||
gem 'hubble', github: 'roidrage/hubble'
|
gem 'hubble', github: 'roidrage/hubble'
|
||||||
gem 'yard-sinatra', github: 'rkh/yard-sinatra'
|
gem 'yard-sinatra', github: 'rkh/yard-sinatra'
|
||||||
gem 'gh', github: 'rkh/gh'
|
gem 'gh', github: 'rkh/gh'
|
||||||
|
gem 'bunny'
|
||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem 'rspec', '~> 2.11'
|
gem 'rspec', '~> 2.11'
|
||||||
|
|
82
Gemfile.lock
82
Gemfile.lock
|
@ -6,7 +6,7 @@ GIT
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/rkh/gh.git
|
remote: git://github.com/rkh/gh.git
|
||||||
revision: 5aa120dd493f1430fc1af9d97363daea5c4c3415
|
revision: affde20a4fecb1023f2e7031734b9386a76d22c2
|
||||||
specs:
|
specs:
|
||||||
gh (0.8.0)
|
gh (0.8.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -25,14 +25,16 @@ GIT
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/roidrage/hubble.git
|
remote: git://github.com/roidrage/hubble.git
|
||||||
revision: 5220415d5542a2868d54f7be9f35fc1d66126b8e
|
revision: 8972b940a4f927927d2a4bdb250b3c98c04692a6
|
||||||
specs:
|
specs:
|
||||||
hubble (0.1.2)
|
hubble (0.1.2)
|
||||||
|
faraday
|
||||||
json (~> 1.6.5)
|
json (~> 1.6.5)
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/travis-ci/travis-core.git
|
remote: git://github.com/travis-ci/travis-core.git
|
||||||
revision: 73679d7263ded28620dac7815e4aed253a8191d3
|
revision: ea7a1678a0388e586ac4778a9b6ee56a11dfb0aa
|
||||||
|
branch: sf-more-services
|
||||||
specs:
|
specs:
|
||||||
travis-core (0.0.1)
|
travis-core (0.0.1)
|
||||||
actionmailer (~> 3.2.3)
|
actionmailer (~> 3.2.3)
|
||||||
|
@ -53,7 +55,7 @@ GIT
|
||||||
|
|
||||||
GIT
|
GIT
|
||||||
remote: git://github.com/travis-ci/travis-support.git
|
remote: git://github.com/travis-ci/travis-support.git
|
||||||
revision: b150763d253331de9adadcb5b39f7df5efccb676
|
revision: 06844d2db558d88be775ca1cf9cfff8ec36120fb
|
||||||
specs:
|
specs:
|
||||||
travis-support (0.0.1)
|
travis-support (0.0.1)
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@ PATH
|
||||||
hubble (~> 0.1)
|
hubble (~> 0.1)
|
||||||
newrelic_rpm (~> 3.3.0)
|
newrelic_rpm (~> 3.3.0)
|
||||||
pg (~> 0.13.2)
|
pg (~> 0.13.2)
|
||||||
|
rack-contrib (~> 1.1)
|
||||||
rack-ssl (~> 1.3)
|
rack-ssl (~> 1.3)
|
||||||
redcarpet (~> 2.1)
|
redcarpet (~> 2.1)
|
||||||
sinatra (~> 1.3)
|
sinatra (~> 1.3)
|
||||||
|
@ -76,60 +79,57 @@ PATH
|
||||||
GEM
|
GEM
|
||||||
remote: http://rubygems.org/
|
remote: http://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actionmailer (3.2.6)
|
actionmailer (3.2.8)
|
||||||
actionpack (= 3.2.6)
|
actionpack (= 3.2.8)
|
||||||
mail (~> 2.4.4)
|
mail (~> 2.4.4)
|
||||||
actionpack (3.2.6)
|
actionpack (3.2.8)
|
||||||
activemodel (= 3.2.6)
|
activemodel (= 3.2.8)
|
||||||
activesupport (= 3.2.6)
|
activesupport (= 3.2.8)
|
||||||
builder (~> 3.0.0)
|
builder (~> 3.0.0)
|
||||||
erubis (~> 2.7.0)
|
erubis (~> 2.7.0)
|
||||||
journey (~> 1.0.1)
|
journey (~> 1.0.4)
|
||||||
rack (~> 1.4.0)
|
rack (~> 1.4.0)
|
||||||
rack-cache (~> 1.2)
|
rack-cache (~> 1.2)
|
||||||
rack-test (~> 0.6.1)
|
rack-test (~> 0.6.1)
|
||||||
sprockets (~> 2.1.3)
|
sprockets (~> 2.1.3)
|
||||||
activemodel (3.2.6)
|
activemodel (3.2.8)
|
||||||
activesupport (= 3.2.6)
|
activesupport (= 3.2.8)
|
||||||
builder (~> 3.0.0)
|
builder (~> 3.0.0)
|
||||||
activerecord (3.2.6)
|
activerecord (3.2.8)
|
||||||
activemodel (= 3.2.6)
|
activemodel (= 3.2.8)
|
||||||
activesupport (= 3.2.6)
|
activesupport (= 3.2.8)
|
||||||
arel (~> 3.0.2)
|
arel (~> 3.0.2)
|
||||||
tzinfo (~> 0.3.29)
|
tzinfo (~> 0.3.29)
|
||||||
activesupport (3.2.6)
|
activesupport (3.2.8)
|
||||||
i18n (~> 0.6)
|
i18n (~> 0.6)
|
||||||
multi_json (~> 1.0)
|
multi_json (~> 1.0)
|
||||||
addressable (2.3.2)
|
addressable (2.3.2)
|
||||||
arel (3.0.2)
|
arel (3.0.2)
|
||||||
atomic (1.0.1)
|
atomic (1.0.1)
|
||||||
avl_tree (1.1.3)
|
avl_tree (1.1.3)
|
||||||
backports (2.6.2)
|
backports (2.6.4)
|
||||||
builder (3.0.0)
|
builder (3.0.3)
|
||||||
daemons (1.1.8)
|
bunny (0.8.0)
|
||||||
|
daemons (1.1.9)
|
||||||
data_migrations (0.0.1)
|
data_migrations (0.0.1)
|
||||||
activerecord
|
activerecord
|
||||||
rake
|
rake
|
||||||
diff-lcs (1.1.3)
|
diff-lcs (1.1.3)
|
||||||
erubis (2.7.0)
|
erubis (2.7.0)
|
||||||
eventmachine (0.12.10)
|
eventmachine (1.0.0)
|
||||||
factory_girl (2.4.2)
|
factory_girl (2.4.2)
|
||||||
activesupport
|
activesupport
|
||||||
faraday (0.8.1)
|
faraday (0.8.4)
|
||||||
multipart-post (~> 1.1)
|
multipart-post (~> 1.1)
|
||||||
ffi (1.1.0)
|
foreman (0.59.0)
|
||||||
foreman (0.53.0)
|
|
||||||
thor (>= 0.13.6)
|
thor (>= 0.13.6)
|
||||||
hashr (0.0.21)
|
hashr (0.0.22)
|
||||||
hike (1.2.1)
|
hike (1.2.1)
|
||||||
hitimes (1.1.1)
|
hitimes (1.1.1)
|
||||||
i18n (0.6.0)
|
i18n (0.6.1)
|
||||||
journey (1.0.4)
|
journey (1.0.4)
|
||||||
json (1.6.7)
|
json (1.6.7)
|
||||||
listen (0.4.7)
|
listen (0.5.1)
|
||||||
rb-fchange (~> 0.0.5)
|
|
||||||
rb-fsevent (~> 0.9.1)
|
|
||||||
rb-inotify (~> 0.8.8)
|
|
||||||
mail (2.4.4)
|
mail (2.4.4)
|
||||||
i18n (>= 0.4.0)
|
i18n (>= 0.4.0)
|
||||||
mime-types (~> 1.16)
|
mime-types (~> 1.16)
|
||||||
|
@ -140,7 +140,7 @@ GEM
|
||||||
avl_tree (~> 1.1.2)
|
avl_tree (~> 1.1.2)
|
||||||
hitimes (~> 1.1)
|
hitimes (~> 1.1)
|
||||||
mime-types (1.19)
|
mime-types (1.19)
|
||||||
mocha (0.12.3)
|
mocha (0.12.4)
|
||||||
metaclass (~> 0.0.1)
|
metaclass (~> 0.0.1)
|
||||||
multi_json (1.3.6)
|
multi_json (1.3.6)
|
||||||
multipart-post (1.1.5)
|
multipart-post (1.1.5)
|
||||||
|
@ -162,25 +162,22 @@ GEM
|
||||||
rack (1.4.1)
|
rack (1.4.1)
|
||||||
rack-cache (1.2)
|
rack-cache (1.2)
|
||||||
rack (>= 0.4)
|
rack (>= 0.4)
|
||||||
|
rack-contrib (1.1.0)
|
||||||
|
rack (>= 0.9.1)
|
||||||
rack-protection (1.2.0)
|
rack-protection (1.2.0)
|
||||||
rack
|
rack
|
||||||
rack-ssl (1.3.2)
|
rack-ssl (1.3.2)
|
||||||
rack
|
rack
|
||||||
rack-test (0.6.1)
|
rack-test (0.6.1)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
railties (3.2.6)
|
railties (3.2.8)
|
||||||
actionpack (= 3.2.6)
|
actionpack (= 3.2.8)
|
||||||
activesupport (= 3.2.6)
|
activesupport (= 3.2.8)
|
||||||
rack-ssl (~> 1.3.2)
|
rack-ssl (~> 1.3.2)
|
||||||
rake (>= 0.8.7)
|
rake (>= 0.8.7)
|
||||||
rdoc (~> 3.4)
|
rdoc (~> 3.4)
|
||||||
thor (>= 0.14.6, < 2.0)
|
thor (>= 0.14.6, < 2.0)
|
||||||
rake (0.9.2.2)
|
rake (0.9.2.2)
|
||||||
rb-fchange (0.0.5)
|
|
||||||
ffi
|
|
||||||
rb-fsevent (0.9.1)
|
|
||||||
rb-inotify (0.8.8)
|
|
||||||
ffi (>= 0.5.0)
|
|
||||||
rdoc (3.12)
|
rdoc (3.12)
|
||||||
json (~> 1.4)
|
json (~> 1.4)
|
||||||
redcarpet (2.1.1)
|
redcarpet (2.1.1)
|
||||||
|
@ -193,14 +190,14 @@ GEM
|
||||||
rspec-expectations (~> 2.11.0)
|
rspec-expectations (~> 2.11.0)
|
||||||
rspec-mocks (~> 2.11.0)
|
rspec-mocks (~> 2.11.0)
|
||||||
rspec-core (2.11.1)
|
rspec-core (2.11.1)
|
||||||
rspec-expectations (2.11.2)
|
rspec-expectations (2.11.3)
|
||||||
diff-lcs (~> 1.1.3)
|
diff-lcs (~> 1.1.3)
|
||||||
rspec-mocks (2.11.1)
|
rspec-mocks (2.11.3)
|
||||||
signature (0.1.3)
|
signature (0.1.4)
|
||||||
simple_states (0.1.1)
|
simple_states (0.1.1)
|
||||||
activesupport
|
activesupport
|
||||||
hashr (~> 0.0.10)
|
hashr (~> 0.0.10)
|
||||||
sinatra (1.3.2)
|
sinatra (1.3.3)
|
||||||
rack (~> 1.3, >= 1.3.6)
|
rack (~> 1.3, >= 1.3.6)
|
||||||
rack-protection (~> 1.2)
|
rack-protection (~> 1.2)
|
||||||
tilt (~> 1.3, >= 1.3.3)
|
tilt (~> 1.3, >= 1.3.3)
|
||||||
|
@ -231,6 +228,7 @@ PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
|
bunny
|
||||||
factory_girl (~> 2.4.0)
|
factory_girl (~> 2.4.0)
|
||||||
foreman
|
foreman
|
||||||
gh!
|
gh!
|
||||||
|
|
7
docs/00_overview.md
Normal file
7
docs/00_overview.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
... some general docs here ...
|
||||||
|
|
||||||
|
## Media Types
|
||||||
|
|
||||||
|
The API is currently [JSON](http://en.wikipedia.org/wiki/JSON) only.
|
45
docs/01_cross_origin.md
Normal file
45
docs/01_cross_origin.md
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
# Web Clients
|
||||||
|
|
||||||
|
When writing an in-browser client, you have to circumvent the browser's
|
||||||
|
[same origin policy](http://en.wikipedia.org/wiki/Same_origin_policy).
|
||||||
|
Generally, we offer two different approaches for this:
|
||||||
|
[Cross-Origin Resource Sharing](http://en.wikipedia.org/wiki/Cross-origin_resource_sharing) (aka CORS)
|
||||||
|
and [JSONP](http://en.wikipedia.org/wiki/JSONP). If you don't have any good
|
||||||
|
reason for using JSONP, we recommend you use CORS.
|
||||||
|
|
||||||
|
## Cross-Origin Resource Sharing
|
||||||
|
|
||||||
|
All API resources set appropriate headers to allow Cross-Origin requests. Be
|
||||||
|
aware that on Internet Explorer you might have to use a different interface to
|
||||||
|
send these requests.
|
||||||
|
|
||||||
|
// using XMLHttpRequest or XDomainRequest to send an API request
|
||||||
|
var invocation = window.XDomainRequest ? new XDomainRequest() : new XMLHttpRequest();
|
||||||
|
|
||||||
|
if(invocation) {
|
||||||
|
invocation.open("GET", "https://api.travis-ci.org/", true);
|
||||||
|
invocation.onreadystatechange = function() { alert("it worked!") };
|
||||||
|
invocation.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
In contrast to JSONP, CORS does not lead to any execution of untrusted code.
|
||||||
|
|
||||||
|
Most JavaScript frameworks, like [jQuery](http://jquery.com), take care of CORS
|
||||||
|
requests for you under the hood, so you can just do a normal *ajax* request.
|
||||||
|
|
||||||
|
// using jQuery
|
||||||
|
$.get("https://api.travis-ci.org/", function() { alert("it worked!") });
|
||||||
|
|
||||||
|
Our current setup allows the headers `Content-Type`, `Authorization`, `Accept` and the HTTP methods `HEAD`, `GET`, `POST`, `PATCH`, `PUT`, `DELETE`.
|
||||||
|
|
||||||
|
## JSONP
|
||||||
|
|
||||||
|
You can disable the same origin policy by treating the response as JavaScript.
|
||||||
|
Supply a `callback` parameter to use this.
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function jsonpCallback() { alert("it worked!") };
|
||||||
|
</script>
|
||||||
|
<script src="https://api.travis-ci.org/?callback=jsonpCallback"></script>
|
||||||
|
|
||||||
|
This has the potential of code injection, use with caution.
|
|
@ -6,6 +6,7 @@ require 'travis'
|
||||||
require 'backports'
|
require 'backports'
|
||||||
require 'rack'
|
require 'rack'
|
||||||
require 'rack/protection'
|
require 'rack/protection'
|
||||||
|
require 'rack/contrib'
|
||||||
require 'active_record'
|
require 'active_record'
|
||||||
require 'redis'
|
require 'redis'
|
||||||
require 'gh'
|
require 'gh'
|
||||||
|
@ -52,8 +53,13 @@ class Travis::Api::App
|
||||||
@app = Rack::Builder.app do
|
@app = Rack::Builder.app do
|
||||||
use Rack::Protection::PathTraversal
|
use Rack::Protection::PathTraversal
|
||||||
use Rack::SSL if Endpoint.production?
|
use Rack::SSL if Endpoint.production?
|
||||||
|
use Rack::JSONP
|
||||||
use ActiveRecord::ConnectionAdapters::ConnectionManagement
|
use ActiveRecord::ConnectionAdapters::ConnectionManagement
|
||||||
|
|
||||||
|
use Rack::Config do |env|
|
||||||
|
env['travis.global_prefix'] = env['SCRIPT_NAME']
|
||||||
|
end
|
||||||
|
|
||||||
Middleware.subclasses.each { |m| use(m) }
|
Middleware.subclasses.each { |m| use(m) }
|
||||||
Endpoint.subclasses.each { |e| map(e.prefix) { run(e.new) } }
|
Endpoint.subclasses.each { |e| map(e.prefix) { run(e.new) } }
|
||||||
end
|
end
|
||||||
|
@ -75,6 +81,10 @@ class Travis::Api::App
|
||||||
|
|
||||||
def self.setup_travis
|
def self.setup_travis
|
||||||
Travis::Database.connect
|
Travis::Database.connect
|
||||||
|
|
||||||
|
Travis::Services.constants.each do |name|
|
||||||
|
Travis.services[name.to_s.underscore.to_sym] = Travis::Services.const_get(name) unless name == :Base
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.load_endpoints
|
def self.load_endpoints
|
||||||
|
|
|
@ -4,7 +4,7 @@ require 'securerandom'
|
||||||
class Travis::Api::App
|
class Travis::Api::App
|
||||||
class AccessToken
|
class AccessToken
|
||||||
DEFAULT_SCOPES = [:public, :private]
|
DEFAULT_SCOPES = [:public, :private]
|
||||||
attr_reader :token, :scopes, :user_id
|
attr_reader :token, :scopes, :user_id, :app_id
|
||||||
|
|
||||||
def self.create(options = {})
|
def self.create(options = {})
|
||||||
new(options).tap(&:save)
|
new(options).tap(&:save)
|
||||||
|
@ -12,22 +12,25 @@ class Travis::Api::App
|
||||||
|
|
||||||
def self.find_by_token(token)
|
def self.find_by_token(token)
|
||||||
user_id, app_id, *scopes = redis.lrange(key(token), 0, -1)
|
user_id, app_id, *scopes = redis.lrange(key(token), 0, -1)
|
||||||
new(token: token, scopes: scopes, user_id: user_id) if user_id
|
new(token: token, scopes: scopes, user_id: user_id, app_id: app_id) if user_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
raise ArgumentError, 'must supply either user_id or user' unless options.key?(:user) ^ options.key?(:user_id)
|
raise ArgumentError, 'must supply either user_id or user' unless options.key?(:user) ^ options.key?(:user_id)
|
||||||
|
raise ArgumentError, 'must supply app_id' unless options.key?(:app_id)
|
||||||
|
|
||||||
@token = options[:token] || SecureRandom.urlsafe_base64(64)
|
@app_id = Integer(options[:app_id])
|
||||||
@scopes = Array(options[:scopes] || options[:scope] || DEFAULT_SCOPES).map(&:to_sym)
|
@scopes = Array(options[:scopes] || options[:scope] || DEFAULT_SCOPES).map(&:to_sym)
|
||||||
@user = options[:user]
|
@user = options[:user]
|
||||||
@user_id = Integer(options[:user_id] || @user.id)
|
@user_id = Integer(options[:user_id] || @user.id)
|
||||||
|
@token = options[:token] || reuse_token || SecureRandom.urlsafe_base64(16)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save
|
def save
|
||||||
key = key(token)
|
key = key(token)
|
||||||
redis.del(key)
|
redis.del(key)
|
||||||
redis.rpush(key, [user_id, '', *scopes].map(&:to_s))
|
redis.rpush(key, [user_id, app_id, *scopes].map(&:to_s))
|
||||||
|
redis.set(reuse_key, token)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
|
@ -55,5 +58,19 @@ class Travis::Api::App
|
||||||
|
|
||||||
include Helpers
|
include Helpers
|
||||||
extend Helpers
|
extend Helpers
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def reuse_token
|
||||||
|
redis.get(reuse_key)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reuse_key
|
||||||
|
@reuse_key ||= begin
|
||||||
|
keys = ["r", user_id, app_id]
|
||||||
|
keys.append(scopes.map(&:to_s).sort) if scopes != DEFAULT_SCOPES
|
||||||
|
keys.join(':')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
require 'travis/api/app'
|
require 'travis/api/app'
|
||||||
|
require 'addressable/uri'
|
||||||
|
|
||||||
class Travis::Api::App
|
class Travis::Api::App
|
||||||
# Superclass for HTTP endpoints. Takes care of prefixing.
|
# Superclass for HTTP endpoints. Takes care of prefixing.
|
||||||
|
@ -10,5 +11,32 @@ class Travis::Api::App
|
||||||
before { content_type :json }
|
before { content_type :json }
|
||||||
error(ActiveRecord::RecordNotFound, Sinatra::NotFound) { not_found }
|
error(ActiveRecord::RecordNotFound, Sinatra::NotFound) { not_found }
|
||||||
not_found { content_type =~ /json/ ? { 'file' => 'not found' } : 'file not found' }
|
not_found { content_type =~ /json/ ? { 'file' => 'not found' } : 'file not found' }
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def service(key, user = current_user)
|
||||||
|
const = Travis.services[key] || raise("no service registered for #{key}")
|
||||||
|
const.new(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
env['travis.access_token'].user if env['travis.access_token']
|
||||||
|
end
|
||||||
|
|
||||||
|
def redis
|
||||||
|
Thread.current[:redis] ||= ::Redis.connect(url: Travis.config.redis.url)
|
||||||
|
end
|
||||||
|
|
||||||
|
def endpoint(link, query_values = {})
|
||||||
|
link = url(File.join(env['travis.global_prefix'], link), true, false)
|
||||||
|
uri = Addressable::URI.parse(link)
|
||||||
|
query_values = query_values.merge(uri.query_values) if uri.query_values
|
||||||
|
uri.query_values = query_values
|
||||||
|
uri.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def safe_redirect(url)
|
||||||
|
redirect(endpoint('/redirect', to: url), 301)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,9 @@ class Travis::Api::App
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
class Artifacts < Endpoint
|
class Artifacts < Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get('/:id') { |id| body Artifact.find(id) }
|
get('/:id') do |id|
|
||||||
|
body service(:artifacts).find_one(params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
require 'travis/api/app'
|
require 'travis/api/app'
|
||||||
require 'addressable/uri'
|
require 'addressable/uri'
|
||||||
require 'faraday'
|
require 'faraday'
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
class Travis::Api::App
|
class Travis::Api::App
|
||||||
class Endpoint
|
class Endpoint
|
||||||
|
@ -17,6 +18,7 @@ class Travis::Api::App
|
||||||
# authorize) against GitHub.
|
# authorize) against GitHub.
|
||||||
#
|
#
|
||||||
# This is the recommended way for third-party web apps.
|
# This is the recommended way for third-party web apps.
|
||||||
|
# The entry point is [/auth/authorize](#/auth/authorize).
|
||||||
#
|
#
|
||||||
# ## GitHub Token
|
# ## GitHub Token
|
||||||
#
|
#
|
||||||
|
@ -28,13 +30,21 @@ class Travis::Api::App
|
||||||
# This is the recommended way for GitHub applications that also want Travis
|
# This is the recommended way for GitHub applications that also want Travis
|
||||||
# integration.
|
# integration.
|
||||||
#
|
#
|
||||||
|
# The entry point is [/auth/github](#/auth/github).
|
||||||
|
#
|
||||||
# ## Cross-Origin Window Messages
|
# ## Cross-Origin Window Messages
|
||||||
#
|
#
|
||||||
# This is the recommended way for the official client. We might improve the
|
# This is the recommended way for the official client. We might improve the
|
||||||
# authorization flow to support third-party clients in the future, too.
|
# authorization flow to support third-party clients in the future, too.
|
||||||
|
#
|
||||||
|
# The entry point is [/auth/post_message](#/auth/post_message).
|
||||||
class Authorization < Endpoint
|
class Authorization < Endpoint
|
||||||
set prefix: '/auth'
|
set prefix: '/auth'
|
||||||
|
enable :inline_templates
|
||||||
|
|
||||||
|
# Endpoint for retrieving an authorization code, which in turn can be used
|
||||||
|
# to generate an access token.
|
||||||
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
#
|
#
|
||||||
# * **client_id**: your App's client id (required)
|
# * **client_id**: your App's client id (required)
|
||||||
|
@ -45,45 +55,68 @@ class Travis::Api::App
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Endpoint for generating an access token from an authorization code.
|
||||||
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
#
|
#
|
||||||
# * **client_id**: your App's client id (required)
|
# * **client_id**: your App's client id (required)
|
||||||
# * **client_secret**: your App's client secret (required)
|
# * **client_secret**: your App's client secret (required)
|
||||||
# * **code**: code retrieved from redirect from [/authorize](#/authorize) (required)
|
# * **code**: code retrieved from redirect from [/auth/authorize](#/auth/authorize) (required)
|
||||||
# * **redirect_uri**: URL to redirect to
|
# * **redirect_uri**: URL to redirect to
|
||||||
# * **state**: same value sent to [/authorize](#/authorize)
|
# * **state**: same value sent to [/auth/authorize](#/auth/authorize)
|
||||||
post '/access_token' do
|
post '/access_token' do
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Endpoint for generating an access token from a GitHub access token.
|
||||||
|
#
|
||||||
# Parameters:
|
# Parameters:
|
||||||
#
|
#
|
||||||
# * **token**: GitHub token for checking authorization (required)
|
# * **token**: GitHub token for checking authorization (required)
|
||||||
post '/github' do
|
post '/github' do
|
||||||
{ 'access_token' => github_to_travis(params[:token]) }
|
{ 'access_token' => github_to_travis(params[:token], app_id: 1) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Endpoint for making sure user authorized Travis CI to access GitHub.
|
||||||
|
# There are no restrictions on where to redirect to after handshake.
|
||||||
|
# However, no information whatsoever is being sent with the redirect.
|
||||||
|
#
|
||||||
|
# Parameters:
|
||||||
|
#
|
||||||
|
# * **redirect_uri**: URI to redirect to after handshake.
|
||||||
|
get '/handshake' do
|
||||||
|
handshake do |*, redirect_uri|
|
||||||
|
safe_redirect redirect_uri
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# This endpoint is meant to be embedded in an iframe, popup window or
|
||||||
|
# similar. It will perform the handshake and, once done, will send an
|
||||||
|
# access token and user payload to the parent window via postMessage.
|
||||||
|
#
|
||||||
|
# However, the endpoint to send the payload to has to be explicitely
|
||||||
|
# whitelisted in production, as this is endpoint is only meant to be used
|
||||||
|
# with the official Travis CI client at the moment.
|
||||||
|
#
|
||||||
|
# Example usage:
|
||||||
|
#
|
||||||
|
# window.addEventListener("message", function(event) {
|
||||||
|
# alert("received token: " + event.data.token);
|
||||||
|
# });
|
||||||
|
#
|
||||||
|
# var iframe = $('<iframe />').hide();
|
||||||
|
# iframe.appendTo('body');
|
||||||
|
# iframe.attr('src', "https://api.travis-ci.org/auth/post_message");
|
||||||
|
#
|
||||||
|
# Note that embedding it in an iframe will only work for users that are
|
||||||
|
# logged in at GitHub and already authorized Travis CI. It is therefore
|
||||||
|
# recommended to redirect to [/auth/handshake](#/auth/handshake) if no
|
||||||
|
# token is being received.
|
||||||
get '/post_message' do
|
get '/post_message' do
|
||||||
config = Travis.config.oauth2
|
handshake do |user, token, target_origin|
|
||||||
endpoint = Addressable::URI.parse(config.authorization_server)
|
halt 403, invalid_target(target_origin) unless target_ok? target_origin
|
||||||
values = {
|
rendered_user = Travis::Api.data(service(:user, user).find_one, type: :user, version: :v2)
|
||||||
client_id: config.client_id,
|
post_message(token: token, user: rendered_user, target_origin: target_origin)
|
||||||
scope: config.scope,
|
|
||||||
redirect_uri: url
|
|
||||||
}
|
|
||||||
|
|
||||||
if params[:code]
|
|
||||||
endpoint.path = config.access_token_path
|
|
||||||
values[:code] = params[:code]
|
|
||||||
values[:state] = params[:state] if params[:state]
|
|
||||||
values[:client_secret] = config.client_secret
|
|
||||||
|
|
||||||
token = github_to_travis get_token(endpoint.to_s, values)
|
|
||||||
{ 'access_token' => token }
|
|
||||||
else
|
|
||||||
endpoint.path = config.authorize_path
|
|
||||||
endpoint.query_values = values
|
|
||||||
redirect to(endpoint.to_s)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -93,19 +126,70 @@ class Travis::Api::App
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def github_to_travis(token)
|
def handshake
|
||||||
|
config = Travis.config.oauth2
|
||||||
|
endpoint = Addressable::URI.parse(config.authorization_server)
|
||||||
|
values = {
|
||||||
|
client_id: config.client_id,
|
||||||
|
scope: config.scope,
|
||||||
|
redirect_uri: url
|
||||||
|
}
|
||||||
|
|
||||||
|
if params[:code] and state_ok?(params[:state])
|
||||||
|
endpoint.path = config.access_token_path
|
||||||
|
values[:state] = params[:state]
|
||||||
|
values[:code] = params[:code]
|
||||||
|
values[:client_secret] = config.client_secret
|
||||||
|
github_token = get_token(endpoint.to_s, values)
|
||||||
|
user = user_for_github_token(github_token)
|
||||||
|
token = generate_token(user: user, app_id: 0)
|
||||||
|
payload = params[:state].split(":::", 2)[1]
|
||||||
|
yield user, token, payload
|
||||||
|
else
|
||||||
|
values[:state] = create_state
|
||||||
|
endpoint.path = config.authorize_path
|
||||||
|
endpoint.query_values = values
|
||||||
|
redirect to(endpoint.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_state
|
||||||
|
state = SecureRandom.urlsafe_base64(16)
|
||||||
|
redis.sadd('github:states', state)
|
||||||
|
redis.expire('github:states', 1800)
|
||||||
|
payload = params[:origin] || params[:redirect_uri]
|
||||||
|
state << ":::" << payload if payload
|
||||||
|
state
|
||||||
|
end
|
||||||
|
|
||||||
|
def state_ok?(state)
|
||||||
|
redis.srem('github:states', state.split(":::", 1)) if state
|
||||||
|
end
|
||||||
|
|
||||||
|
def github_to_travis(token, options = {})
|
||||||
|
generate_token options.merge(user: user_for_github_token(token))
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_info(data, misc = {})
|
||||||
|
info = data.to_hash.slice('name', 'login', 'github_oauth_token', 'gravatar_id')
|
||||||
|
info.merge! misc
|
||||||
|
info['github_id'] ||= data['id']
|
||||||
|
info
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_for_github_token(token)
|
||||||
data = GH.with(token: token.to_s) { GH['user'] }
|
data = GH.with(token: token.to_s) { GH['user'] }
|
||||||
scopes = parse_scopes data.headers['x-oauth-scopes']
|
scopes = parse_scopes data.headers['x-oauth-scopes']
|
||||||
user = User.find_by_login(data['login'])
|
user = User.find_by_github_id(data['id'])
|
||||||
|
user ||= User.create! user_info(data, github_oauth_token: token)
|
||||||
|
|
||||||
halt 403, 'not a Travis user' if user.nil?
|
halt 403, 'not a Travis user' if user.nil?
|
||||||
halt 403, 'insufficient access' unless acceptable? scopes
|
halt 403, 'insufficient access' unless acceptable? scopes
|
||||||
|
user
|
||||||
generate_token(user)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_token(endoint, value)
|
def get_token(endoint, values)
|
||||||
response = Faraday.get(endoint, value)
|
response = Faraday.post(endoint, values)
|
||||||
parameters = Addressable::URI.form_unencode(response.body)
|
parameters = Addressable::URI.form_unencode(response.body)
|
||||||
parameters.assoc("access_token").last
|
parameters.assoc("access_token").last
|
||||||
end
|
end
|
||||||
|
@ -114,13 +198,44 @@ class Travis::Api::App
|
||||||
data.gsub(/\s/,'').split(',') if data
|
data.gsub(/\s/,'').split(',') if data
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_token(user)
|
def generate_token(options)
|
||||||
AccessToken.create(user: user).token
|
AccessToken.create(options).token
|
||||||
end
|
end
|
||||||
|
|
||||||
def acceptable?(scopes)
|
def acceptable?(scopes)
|
||||||
scopes.include? 'public_repo' or scopes.include? 'repo'
|
scopes.include? 'public_repo' or scopes.include? 'repo'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def post_message(payload)
|
||||||
|
content_type :html
|
||||||
|
erb(:post_message, locals: payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def invalid_target(target_origin)
|
||||||
|
content_type :html
|
||||||
|
erb(:invalid_target, {}, target_origin: target_origin)
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_ok?(target_origin)
|
||||||
|
target_origin =~ %r{
|
||||||
|
^ http:// (localhost|127\.0\.0\.1)(:\d+)? $ |
|
||||||
|
^ https:// (\w+\.)?travis-ci\.(org|com) $
|
||||||
|
}x
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
__END__
|
||||||
|
|
||||||
|
@@ invalid_target
|
||||||
|
<script>
|
||||||
|
alert('refusing to send a token to <%= target_origin.inspect %>, not whitelisted!');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@@ post_message
|
||||||
|
<script>
|
||||||
|
var payload = <%= render_json(user) %>;
|
||||||
|
payload.token = <%= token.inspect %>;
|
||||||
|
window.parent.postMessage(payload, <%= target_origin.inspect %>);
|
||||||
|
</script>
|
||||||
|
|
|
@ -6,14 +6,7 @@ class Travis::Api::App
|
||||||
class Branches < Endpoint
|
class Branches < Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get('/') do
|
get('/') do
|
||||||
body repository, :type => "Branches"
|
body service(:branches).find_all(params), type: :branches
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def repository
|
|
||||||
pass if params.empty?
|
|
||||||
Repository.find_by(params) || not_found
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,22 +6,12 @@ class Travis::Api::App
|
||||||
class Builds < Endpoint
|
class Builds < Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get '/' do
|
get '/' do
|
||||||
scope = repository.builds.by_event_type(params[:event_type] || 'push')
|
body service(:builds).find_all(params)
|
||||||
scope = params[:after] ? scope.older_than(params[:after]) : scope.recent
|
|
||||||
scope
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get '/:id' do
|
get '/:id' do
|
||||||
one = params[:repository_id] ? repository.builds : Build
|
body service(:builds).find_one(params)
|
||||||
body one.includes(:commit, :matrix => [:commit, :log]).find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def repository
|
|
||||||
pass if params.empty?
|
|
||||||
Repository.find_by(params) || not_found
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,11 +7,14 @@ class Travis::Api::App
|
||||||
set prefix: '/docs', public_folder: File.expand_path('../documentation', __FILE__)
|
set prefix: '/docs', public_folder: File.expand_path('../documentation', __FILE__)
|
||||||
enable :inline_templates, :static
|
enable :inline_templates, :static
|
||||||
|
|
||||||
|
# Don't cache general docs in development
|
||||||
|
configure(:development) { before { @@general_docs = nil } }
|
||||||
|
|
||||||
# HTML view for [/endpoints](#/endpoints/).
|
# HTML view for [/endpoints](#/endpoints/).
|
||||||
get '/' do
|
get '/' do
|
||||||
content_type :html
|
content_type :html
|
||||||
endpoints = Endpoints.endpoints
|
endpoints = Endpoints.endpoints
|
||||||
erb :index, {}, :endpoints => endpoints.keys.sort.map { |k| endpoints[k] }
|
erb :index, {}, endpoints: endpoints.keys.sort.map { |k| endpoints[k] }
|
||||||
end
|
end
|
||||||
|
|
||||||
helpers do
|
helpers do
|
||||||
|
@ -33,11 +36,38 @@ class Travis::Api::App
|
||||||
end
|
end
|
||||||
|
|
||||||
def docs_for(entry)
|
def docs_for(entry)
|
||||||
markdown(entry['doc']).
|
with_code_highlighting markdown(entry['doc'])
|
||||||
gsub('<pre', '<pre class="prettyprint linenums lang-js pre-scrollable"').
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def with_code_highlighting(str)
|
||||||
|
str.
|
||||||
|
gsub('<pre', '<pre class="prettyprint linenums pre-scrollable"').
|
||||||
gsub(/<\/?code>/, '').
|
gsub(/<\/?code>/, '').
|
||||||
gsub(/TODO:?/, '<span class="label label-warning">TODO</span>')
|
gsub(/TODO:?/, '<span class="label label-warning">TODO</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def general_docs
|
||||||
|
@@general_docs ||= doc_files.map do |file|
|
||||||
|
header, content = File.read(file).split("\n", 2)
|
||||||
|
content = markdown(content)
|
||||||
|
subheaders = []
|
||||||
|
|
||||||
|
content.gsub!(/<h2>(.*)<\/h2>/) do
|
||||||
|
subheaders << $1
|
||||||
|
"<h2 id=\"#{$1}\">#{$1}</h2>"
|
||||||
|
end
|
||||||
|
|
||||||
|
header.gsub! /^#* */, ''
|
||||||
|
{ id: header, title: header, content: with_code_highlighting(content), subheaders: subheaders }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def doc_files
|
||||||
|
pattern = File.expand_path('../../../../../../docs/*.md', __FILE__)
|
||||||
|
Dir[pattern].sort
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -95,6 +125,13 @@ __END__
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body onload="prettyPrint()">
|
<body onload="prettyPrint()">
|
||||||
|
|
||||||
|
<a href="https://github.com/travis-ci/travis-api">
|
||||||
|
<img style="position: absolute; top: 0; right: 0; border: 0;"
|
||||||
|
src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png"
|
||||||
|
alt="Fork me on GitHub">
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<header class="span12">
|
<header class="span12">
|
||||||
|
@ -111,6 +148,13 @@ __END__
|
||||||
</div>
|
</div>
|
||||||
<div class="well" style="padding: 8px 0;">
|
<div class="well" style="padding: 8px 0;">
|
||||||
<ul class="nav nav-list">
|
<ul class="nav nav-list">
|
||||||
|
<% general_docs.each do |doc| %>
|
||||||
|
<li class="nav-header"><a href="#<%= doc[:id] %>"><%= doc[:title] %></a></li>
|
||||||
|
<% doc[:subheaders].each do |sub| %>
|
||||||
|
<li><a href="#<%= sub %>"><%= sub %></a></li>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<li class="divider"></li>
|
||||||
<% endpoints.each do |endpoint| %>
|
<% endpoints.each do |endpoint| %>
|
||||||
<li class="nav-header"><a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a></li>
|
<li class="nav-header"><a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a></li>
|
||||||
<% endpoint['routes'].each do |route| %>
|
<% endpoint['routes'].each do |route| %>
|
||||||
|
@ -156,18 +200,29 @@ __END__
|
||||||
|
|
||||||
<section class="span9">
|
<section class="span9">
|
||||||
|
|
||||||
|
<% general_docs.each do |doc| %>
|
||||||
|
<%= erb :entry, locals: doc %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% endpoints.each do |endpoint| %>
|
<% endpoints.each do |endpoint| %>
|
||||||
<div id="<%= endpoint['name'] %>">
|
<%= erb :entry, {},
|
||||||
<div class="page-header">
|
id: endpoint['name'],
|
||||||
<h1>
|
title: endpoint['name'],
|
||||||
<a href="#<%= endpoint['name'] %>"><%= endpoint['name'] %></a>
|
content: erb(:endpoint_content, {}, endpoint: endpoint) %>
|
||||||
</h1>
|
<% end %>
|
||||||
|
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
<% unless endpoint['doc'].to_s.empty? %>
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
@@ endpoint_content
|
||||||
|
<% unless endpoint['doc'].to_s.empty? %>
|
||||||
<%= docs_for endpoint %>
|
<%= docs_for endpoint %>
|
||||||
<hr>
|
<hr>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% endpoint['routes'].each do |route| %>
|
<% endpoint['routes'].each do |route| %>
|
||||||
<div class="route" id="<%= slug_for(route) %>">
|
<div class="route" id="<%= slug_for(route) %>">
|
||||||
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
|
<pre><h3><%= route['verb'] %> <%= route['uri'] %></h3></pre>
|
||||||
<% if route['scope'] %>
|
<% if route['scope'] %>
|
||||||
|
@ -177,12 +232,15 @@ __END__
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= docs_for route %>
|
<%= docs_for route %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
</section>
|
@@ entry
|
||||||
|
<div id="<%= id %>">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>
|
||||||
|
<a href="#<%= id %>"><%= title %></a>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<%= content %>
|
||||||
</body>
|
</div>
|
||||||
</html>
|
|
||||||
|
|
|
@ -11,6 +11,17 @@ class Travis::Api::App
|
||||||
redirect to('/docs/') if request.preferred_type('application/json', 'text/html') == 'text/html'
|
redirect to('/docs/') if request.preferred_type('application/json', 'text/html') == 'text/html'
|
||||||
{ 'hello' => 'world' }
|
{ 'hello' => 'world' }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Simple endpoints that redirects somewhere else, to make sure we don't
|
||||||
|
# send a referrer.
|
||||||
|
#
|
||||||
|
# Parameters:
|
||||||
|
#
|
||||||
|
# * **to**: URI to redirect to after handshake.
|
||||||
|
get '/redirect' do
|
||||||
|
halt 400 unless params[:to] =~ %r{^https?://}
|
||||||
|
redirect params[:to]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,10 +5,15 @@ class Travis::Api::App
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
class Hooks < Endpoint
|
class Hooks < Endpoint
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add implementation and documentation.
|
||||||
get('/', scope: :private) { raise NotImplementedError }
|
# TODO scope: :private
|
||||||
|
get('/') do
|
||||||
|
body service(:hooks).find_all(params), type: :hooks
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add implementation and documentation.
|
||||||
put('/:id', scope: :admin) { raise NotImplementedError }
|
put('/:id', scope: :admin) do
|
||||||
|
body service(:hooks).update(params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,20 +4,14 @@ class Travis::Api::App
|
||||||
class Endpoint
|
class Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
class Jobs < Endpoint
|
class Jobs < Endpoint
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add documentation.
|
||||||
get('/') do
|
get('/') do
|
||||||
if params[:ids]
|
body service(:jobs).find_all(params)
|
||||||
Job.where(:id => params[:ids]).includes(:commit, :log)
|
|
||||||
else
|
|
||||||
jobs = Job.queued.includes(:commit, :log)
|
|
||||||
jobs = jobs.where(:queue => params[:queue]) if params[:queue]
|
|
||||||
jobs
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add documentation.
|
||||||
get('/:id') do
|
get('/:id') do
|
||||||
body Job.find(params[:id])
|
body service(:jobs).find_one(params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,10 +18,44 @@ class Travis::Api::App
|
||||||
# "synced_at": "2012-08-14T22:11:21Z"
|
# "synced_at": "2012-08-14T22:11:21Z"
|
||||||
# }
|
# }
|
||||||
# }
|
# }
|
||||||
get('/', scope: :private) { body(user) }
|
get '/', scope: :private do
|
||||||
|
body service(:user).find_one, type: :user
|
||||||
|
end
|
||||||
|
|
||||||
|
put '/', scope: :private do
|
||||||
|
raise NotImplementedError
|
||||||
|
update_locale if valid_locale?
|
||||||
|
'ok'
|
||||||
|
end
|
||||||
|
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add implementation and documentation.
|
||||||
post('/sync', scope: :private) { raise NotImplementedError }
|
post '/sync', scope: :private do
|
||||||
|
sync_user(current_user)
|
||||||
|
204
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def sync_user(user)
|
||||||
|
unless user.is_syncing?
|
||||||
|
publisher = Travis::Amqp::Publisher.new('sync.user')
|
||||||
|
publisher.publish({ user_id: user.id }, type: 'sync')
|
||||||
|
user.update_column(:is_syncing, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def locale
|
||||||
|
params[:user][:locale]
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_locale?
|
||||||
|
I18n.available_locales.include?(locale.to_sym) # ???
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_locale
|
||||||
|
current_user.update_attributes!(:locale => locale.to_s)
|
||||||
|
session[:locale] = locale # ???
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,15 +6,17 @@ class Travis::Api::App
|
||||||
class Repositories < Endpoint
|
class Repositories < Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get '/' do
|
get '/' do
|
||||||
scope = Repository.timeline.recent
|
body service(:repositories).find_all(params)
|
||||||
scope = scope.by_owner_name(params[:owner_name]) if params[:owner_name]
|
|
||||||
scope = scope.by_slug(params[:slug]) if params[:slug]
|
|
||||||
scope = scope.search(params[:search]) if params[:search].present?
|
|
||||||
scope
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get('/:id') { body Repository.find_by(params) }
|
get('/:id') do
|
||||||
|
body service(:repositories).find_one(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO make sure status images and cc.xml work
|
||||||
|
# rescue ActiveRecord::RecordNotFound
|
||||||
|
# raise unless params[:format] == 'png'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,12 +6,12 @@ class Travis::Api::App
|
||||||
class Stats < Endpoint
|
class Stats < Endpoint
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get('/repos') do
|
get('/repos') do
|
||||||
{ :stats => Travis::Stats.daily_repository_counts }
|
{ :stats => service(:stats).daily_repository_counts }
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
get('/tests') do
|
get('/tests') do
|
||||||
{ :stats => Travis::Stats.daily_tests_counts }
|
{ :stats => service(:stats).daily_tests_counts }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,9 @@ class Travis::Api::App
|
||||||
# TODO: Add documentation.
|
# TODO: Add documentation.
|
||||||
class Workers < Endpoint
|
class Workers < Endpoint
|
||||||
# TODO: Add implementation and documentation.
|
# TODO: Add implementation and documentation.
|
||||||
get('/') { Worker.order(:host, :name) }
|
get('/') do
|
||||||
|
body service(:workers).find_all(params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ class Travis::Api::App
|
||||||
end
|
end
|
||||||
|
|
||||||
options // do
|
options // do
|
||||||
headers['Access-Control-Allow-Methods'] = "GET, POST, PATCH, PUT, DELETE"
|
headers['Access-Control-Allow-Methods'] = "HEAD, GET, POST, PATCH, PUT, DELETE"
|
||||||
headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization, Accept"
|
headers['Access-Control-Allow-Headers'] = "Content-Type, Authorization, Accept"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,6 @@ cd "$(dirname "$0")/.."
|
||||||
[ $PORT ] || PORT=3000
|
[ $PORT ] || PORT=3000
|
||||||
[ $RACK_ENV ] || RACK_ENV=development
|
[ $RACK_ENV ] || RACK_ENV=development
|
||||||
|
|
||||||
cmd="ruby -I lib -S bundle exec ruby -I lib -S thin start -p $PORT -e $RACK_ENV --threaded"
|
cmd="ruby -I lib -S bundle exec ruby -I lib -S thin start -p $PORT -e $RACK_ENV" #--threaded"
|
||||||
[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -a 127.0.0.1"
|
[[ $RACK_ENV == "development" ]] && exec rerun "$cmd -a 127.0.0.1"
|
||||||
exec $cmd
|
exec $cmd
|
||||||
|
|
|
@ -10,8 +10,9 @@ describe Travis::Api::App::Endpoint::Authorization do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
User.stubs(:find_by_login).with(user.login).returns(user)
|
user.stubs(:github_id).returns(42)
|
||||||
User.stubs(:find).with(user.id).returns(user)
|
User.stubs(:find_github_id).returns(user)
|
||||||
|
User.stubs(:find).returns(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'GET /auth/authorize' do
|
describe 'GET /auth/authorize' do
|
||||||
|
@ -24,9 +25,10 @@ describe Travis::Api::App::Endpoint::Authorization do
|
||||||
|
|
||||||
describe 'POST /auth/github' do
|
describe 'POST /auth/github' do
|
||||||
before do
|
before do
|
||||||
GH.stubs(:with).with(token: 'private repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'repo'})
|
data = { 'id' => user.github_id, 'name' => user.name, 'login' => user.login, 'gravatar_id' => user.gravatar_id }
|
||||||
GH.stubs(:with).with(token: 'public repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'public_repo'})
|
GH.stubs(:with).with(token: 'private repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'repo'}, :to_hash => data)
|
||||||
GH.stubs(:with).with(token: 'no repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'user'})
|
GH.stubs(:with).with(token: 'public repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'public_repo'}, :to_hash => data)
|
||||||
|
GH.stubs(:with).with(token: 'no repos').returns stub(:[] => user.login, :headers => {'x-oauth-scopes' => 'user'}, :to_hash => data)
|
||||||
GH.stubs(:with).with(token: 'invalid token').raises(Faraday::Error::ClientError, 'CLIENT ERROR!')
|
GH.stubs(:with).with(token: 'invalid token').raises(Faraday::Error::ClientError, 'CLIENT ERROR!')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -42,11 +44,11 @@ describe Travis::Api::App::Endpoint::Authorization do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'accepts tokens with repo scope' do
|
it 'accepts tokens with repo scope' do
|
||||||
user_for('private repos').should == user
|
user_for('private repos').name.should == user.name
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'accepts tokens with public_repo scope' do
|
it 'accepts tokens with public_repo scope' do
|
||||||
user_for('public repos').should == user
|
user_for('public repos').name.should == user.name
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'rejects tokens with user scope' do
|
it 'rejects tokens with user scope' do
|
||||||
|
|
|
@ -2,11 +2,12 @@ require 'spec_helper'
|
||||||
|
|
||||||
describe Travis::Api::App::Endpoint::Profile do
|
describe Travis::Api::App::Endpoint::Profile do
|
||||||
include Travis::Testing::Stubs
|
include Travis::Testing::Stubs
|
||||||
let(:access_token) { Travis::Api::App::AccessToken.create(user: user) }
|
let(:access_token) { Travis::Api::App::AccessToken.create(user: user, app_id: -1) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
User.stubs(:find_by_login).with(user.login).returns(user)
|
User.stubs(:find_by_github_id).returns(user)
|
||||||
User.stubs(:find).with(user.id).returns(user)
|
User.stubs(:find).returns(user)
|
||||||
|
user.stubs(:repositories).returns(stub(administratable: stub(select: [repository])))
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'needs to be authenticated' do
|
it 'needs to be authenticated' do
|
||||||
|
@ -15,14 +16,26 @@ describe Travis::Api::App::Endpoint::Profile do
|
||||||
|
|
||||||
it 'replies with the current user' do
|
it 'replies with the current user' do
|
||||||
get('/profile', access_token: access_token.to_s).should be_ok
|
get('/profile', access_token: access_token.to_s).should be_ok
|
||||||
parsed_body["user"].should == {
|
parsed_body['user'].should == {
|
||||||
"login" => user.login,
|
'id' => user.id,
|
||||||
"name" => user.name,
|
'login' => user.login,
|
||||||
"email" => user.email,
|
'name' => user.name,
|
||||||
"gravatar_id" => user.gravatar_id,
|
'email' => user.email,
|
||||||
"locale" => user.locale,
|
'gravatar_id' => user.gravatar_id,
|
||||||
"is_syncing" => user.is_syncing,
|
'locale' => user.locale,
|
||||||
"synced_at" => user.synced_at.strftime('%Y-%m-%dT%H:%M:%SZ')
|
'is_syncing' => user.is_syncing,
|
||||||
|
'synced_at' => user.synced_at.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'includes accounts' do
|
||||||
|
get('/profile', access_token: access_token.to_s).should be_ok
|
||||||
|
parsed_body['accounts'].should == [{
|
||||||
|
'id' => user.id,
|
||||||
|
'login' => user.login,
|
||||||
|
'name' => user.name,
|
||||||
|
'type' => 'user',
|
||||||
|
'reposCount' => nil
|
||||||
|
}]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ describe Travis::Api::App::Extensions::Scoping do
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_scopes(url, *scopes)
|
def with_scopes(url, *scopes)
|
||||||
token = Travis::Api::App::AccessToken.create(user: user, scopes: scopes)
|
token = Travis::Api::App::AccessToken.create(user: user, scopes: scopes, app_id: -1)
|
||||||
get(url, {}, 'travis.access_token' => token)
|
get(url, {}, 'travis.access_token' => token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ describe Travis::Api::App::Middleware::Cors do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets Access-Control-Allow-Methods' do
|
it 'sets Access-Control-Allow-Methods' do
|
||||||
headers['Access-Control-Allow-Methods'].should == "GET, POST, PATCH, PUT, DELETE"
|
headers['Access-Control-Allow-Methods'].should == "HEAD, GET, POST, PATCH, PUT, DELETE"
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets Access-Control-Allow-Headers' do
|
it 'sets Access-Control-Allow-Headers' do
|
||||||
|
|
|
@ -4,7 +4,7 @@ describe Travis::Api::App::Middleware::ScopeCheck do
|
||||||
include Travis::Testing::Stubs
|
include Travis::Testing::Stubs
|
||||||
|
|
||||||
let :access_token do
|
let :access_token do
|
||||||
Travis::Api::App::AccessToken.create(user: user, scope: :foo)
|
Travis::Api::App::AccessToken.create(user: user, scope: :foo, app_id: -1)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|
|
@ -16,9 +16,9 @@ Gem::Specification.new do |s|
|
||||||
|
|
||||||
s.email = [
|
s.email = [
|
||||||
"konstantin.mailinglists@googlemail.com",
|
"konstantin.mailinglists@googlemail.com",
|
||||||
|
"me@svenfuchs.com",
|
||||||
"svenfuchs@artweb-design.de",
|
"svenfuchs@artweb-design.de",
|
||||||
"drogus@gmail.com",
|
"drogus@gmail.com"
|
||||||
"me@svenfuchs.com"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
s.files = [
|
s.files = [
|
||||||
|
@ -27,6 +27,8 @@ Gem::Specification.new do |s|
|
||||||
"Rakefile",
|
"Rakefile",
|
||||||
"config.ru",
|
"config.ru",
|
||||||
"config/database.yml",
|
"config/database.yml",
|
||||||
|
"docs/00_overview.md",
|
||||||
|
"docs/01_cross_origin.md",
|
||||||
"lib/travis/api/app.rb",
|
"lib/travis/api/app.rb",
|
||||||
"lib/travis/api/app/access_token.rb",
|
"lib/travis/api/app/access_token.rb",
|
||||||
"lib/travis/api/app/endpoint.rb",
|
"lib/travis/api/app/endpoint.rb",
|
||||||
|
@ -124,5 +126,6 @@ Gem::Specification.new do |s|
|
||||||
s.add_dependency 'sinatra-contrib', '~> 1.3'
|
s.add_dependency 'sinatra-contrib', '~> 1.3'
|
||||||
s.add_dependency 'redcarpet', '~> 2.1'
|
s.add_dependency 'redcarpet', '~> 2.1'
|
||||||
s.add_dependency 'rack-ssl', '~> 1.3'
|
s.add_dependency 'rack-ssl', '~> 1.3'
|
||||||
|
s.add_dependency 'rack-contrib', '~> 1.1'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user