diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..4b55f9e0 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: ruby + +branches: + only: + - enterprise + - master diff --git a/Gemfile b/Gemfile index 5a482325..76b198e5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' -ruby '2.2.3' if ENV['DYNO'] +ruby '2.3.1' if ENV['DYNO'] -gem 'rails', '4.2.4' +gem 'rails', '4.2.6' gem 'dotenv-rails', '~> 2.0.0', :require => 'dotenv/rails-now' # gem 'pg' gem 'sass-rails', '~> 5.0' @@ -10,15 +10,14 @@ gem 'coffee-rails', '~> 4.1.0' # gem 'therubyracer', platforms: :ruby gem "ember-cli-rails", '0.1.13', require: nil gem 'ember-rails-assets' -#gem "warden-github-rails" , github: "huboard/warden-github-rails" -gem 'warden-github' -# TODO: replace this -gem 'sinatra_auth_github' gem 'rails_12factor', group: :production gem 'puma' gem 'foreman' gem 'sprockets-rails','3.0.0.beta1', :require => 'sprockets/railtie' +# warden +gem 'warden-github', '1.4.0', github: 'huboard/warden-github' + gem 'jquery-rails' gem 'jbuilder', '~> 2.0' gem 'sdoc', '~> 0.4.0', group: :doc @@ -46,6 +45,7 @@ gem 'redcarpet' gem 'pdfkit' gem 'wkhtmltopdf-heroku' gem 'private_pub', '1.0.3' +gem 'sinatra', :require => nil gem 'sidekiq', "~> 3.0" # BUNDLE_WITHOUT="development:test:saas" diff --git a/Gemfile.lock b/Gemfile.lock index 5be344d2..b4e355d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,6 +13,15 @@ PATH enterprise (0.0.1) rails (~> 4.2.0) +GIT + remote: git://github.com/huboard/warden-github.git + revision: 459903681fa00820065348c27639b9f27db9fa55 + specs: + warden-github (1.4.0) + activesupport (> 3.0) + octokit (> 2.1.0) + warden (> 1.0) + PATH remote: vendor/engines/saas specs: @@ -30,42 +39,42 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (2.3.2) - actionmailer (4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) + actionmailer (4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.4) - actionview (= 4.2.4) - activesupport (= 4.2.4) + actionpack (4.2.6) + actionview (= 4.2.6) + activesupport (= 4.2.6) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.4) - activesupport (= 4.2.4) + actionview (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (4.2.4) - activesupport (= 4.2.4) + activejob (4.2.6) + activesupport (= 4.2.6) globalid (>= 0.3.0) - activemodel (4.2.4) - activesupport (= 4.2.4) + activemodel (4.2.6) + activesupport (= 4.2.6) builder (~> 3.1) - activerecord (4.2.4) - activemodel (= 4.2.4) - activesupport (= 4.2.4) + activerecord (4.2.6) + activemodel (= 4.2.6) + activesupport (= 4.2.6) arel (~> 6.0) - activesupport (4.2.4) + activesupport (4.2.6) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - addressable (2.3.7) + addressable (2.4.0) analytics-ruby (2.0.13) annotate (2.6.8) activerecord (>= 3.2, <= 4.3) @@ -126,7 +135,7 @@ GEM sprockets (>= 2.0) ember-rails-assets (0.1.1) erubis (2.7.0) - ethon (0.8.0) + ethon (0.9.0) ffi (>= 1.3.0) eventmachine (1.0.7) excon (0.45.4) @@ -267,7 +276,7 @@ GEM foreman (0.78.0) thor (~> 0.19.1) formatador (0.2.5) - ghee (0.14.19) + ghee (0.14.22) faraday (~> 0.9) faraday_middleware (~> 0.9) hashie (~> 3.3.2) @@ -301,8 +310,8 @@ GEM kgio (2.9.3) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) memcachier (0.0.2) meta_request (0.3.4) callsite (~> 0.0, >= 0.0.11) @@ -315,14 +324,14 @@ GEM minitest (5.8.4) mocha (1.1.0) metaclass (~> 0.0.1) - multi_json (1.11.2) + multi_json (1.12.1) multi_xml (0.5.5) multipart-post (2.0.0) netrc (0.10.3) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) - octokit (3.8.0) - sawyer (~> 0.6.0, >= 0.5.3) + octokit (4.3.0) + sawyer (~> 0.7.0, >= 0.5.3) pdfkit (0.6.2) private_pub (1.0.3) faye @@ -333,8 +342,7 @@ GEM pry-byebug (3.2.0) byebug (~> 5.0) pry (~> 0.10) - puma (2.11.1) - rack (>= 1.1, < 2.0) + puma (3.4.0) rack (1.6.4) rack-attack (4.2.0) rack @@ -344,16 +352,16 @@ GEM rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.4) - actionmailer (= 4.2.4) - actionpack (= 4.2.4) - actionview (= 4.2.4) - activejob (= 4.2.4) - activemodel (= 4.2.4) - activerecord (= 4.2.4) - activesupport (= 4.2.4) + rails (4.2.6) + actionmailer (= 4.2.6) + actionpack (= 4.2.6) + actionview (= 4.2.6) + activejob (= 4.2.6) + activemodel (= 4.2.6) + activerecord (= 4.2.6) + activesupport (= 4.2.6) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.4) + railties (= 4.2.6) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -368,9 +376,9 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.4) rails_stdout_logging (0.0.3) - railties (4.2.4) - actionpack (= 4.2.4) - activesupport (= 4.2.4) + railties (4.2.6) + actionpack (= 4.2.6) + activesupport (= 4.2.6) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.5.0) @@ -413,8 +421,8 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (~> 1.1) - sawyer (0.6.0) - addressable (~> 2.3.5) + sawyer (0.7.0) + addressable (>= 2.3.5, < 2.5) faraday (~> 0.8, < 0.10) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) @@ -425,13 +433,10 @@ GEM json redis (>= 3.0.6) redis-namespace (>= 1.3.1) - sinatra (1.4.5) - rack (~> 1.4) + sinatra (1.4.7) + rack (~> 1.5) rack-protection (~> 1.4) - tilt (~> 1.3, >= 1.3.4) - sinatra_auth_github (1.2.0) - sinatra (~> 1.0) - warden-github (~> 1.2.0) + tilt (>= 1.3, < 3) slop (3.6.0) solid_use_case (2.1.1) deterministic (~> 0.6.0) @@ -468,12 +473,8 @@ GEM execjs (>= 0.3.0) json (>= 1.8.0) uuidtools (2.1.5) - warden (1.2.3) + warden (1.2.6) rack (>= 1.0) - warden-github (1.2.0) - activesupport (> 3.0) - octokit (> 2.1.0) - warden (> 1.0) websocket-driver (0.5.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -518,7 +519,7 @@ DEPENDENCIES pry-byebug puma rack-attack - rails (= 4.2.4) + rails (= 4.2.6) rails_12factor raygun4ruby redcarpet @@ -529,12 +530,12 @@ DEPENDENCIES sass-rails (~> 5.0) sdoc (~> 0.4.0) sidekiq (~> 3.0) - sinatra_auth_github + sinatra solid_use_case spring sprockets-rails (= 3.0.0.beta1) stripe-rails sucker_punch uglifier (>= 1.3.0) - warden-github + warden-github (= 1.4.0)! wkhtmltopdf-heroku diff --git a/README.md b/README.md index 86af1176..1acb0440 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![HuBoard logo](https://raw.github.com/huboard/huboard-web/master/app/assets/images/HuBoardSplash960.png)](https://huboard.com) +[![Build Status](https://travis-ci.org/huboard/huboard-web.svg)](https://travis-ci.org/huboard/huboard-web) Check out our [parent repository](https://github.com/huboard/huboard#github-issues-made-awesome) for details! ## License diff --git a/app/assets/images/GitHub-Mark-Light-32px.png b/app/assets/images/GitHub-Mark-Light-32px.png new file mode 100644 index 00000000..628da97c Binary files /dev/null and b/app/assets/images/GitHub-Mark-Light-32px.png differ diff --git a/app/assets/images/marketing/header-bg--github.png b/app/assets/images/marketing/header-bg--github.png index 119844fe..28927a55 100644 Binary files a/app/assets/images/marketing/header-bg--github.png and b/app/assets/images/marketing/header-bg--github.png differ diff --git a/app/assets/stylesheets/_buttons.scss b/app/assets/stylesheets/_buttons.scss index 2d97afad..e7b6d379 100644 --- a/app/assets/stylesheets/_buttons.scss +++ b/app/assets/stylesheets/_buttons.scss @@ -37,7 +37,14 @@ left: 10px; } } - + + &.hb-octo-button { + img { + position: relative; + right: 7px; + bottom: 1px; + } + } } .hb-button-group { diff --git a/app/assets/stylesheets/_colors.scss b/app/assets/stylesheets/_colors.scss index 2e897247..89f4bf7a 100644 --- a/app/assets/stylesheets/_colors.scss +++ b/app/assets/stylesheets/_colors.scss @@ -2,6 +2,7 @@ $lighterGrey: #CCC; $lightGrey: #999; $grey: #666; $darkGrey: #444; +$navGrey: #F3F3F3; $red: red; diff --git a/app/assets/stylesheets/_repos.scss b/app/assets/stylesheets/_repos.scss index 344e3619..aec9f02e 100644 --- a/app/assets/stylesheets/_repos.scss +++ b/app/assets/stylesheets/_repos.scss @@ -24,6 +24,7 @@ color:rgba(#ee9c49, 0.7); } } + } ul.repos, ul.repos li { @@ -66,4 +67,56 @@ ul.repos li a.btn { margin-top:7px; } +.repo-list { + .label { + &.label-success { + padding: 4px; + } + &.label-warning { + padding: 4px; + } + &.label-warning-grey { + padding: 4px; + background-color: $lightGrey; + } + } + + .nav-tabs { + .disabled { + cursor: not-allowed; + a { + pointer-events: none; + } + } + } +} + +.dashboard--alert { + h5 { + color: black; + i.ui-icon-alert { + position: relative; + top: 1px; + color: #ee9c49; + } + } + &.public-only-hero { + position: relative; + bottom: 10px; + text-align: center; + } + &.public-only-hero:before { + content: ''; + width: 0px; height: 5px; + margin: -19px 25px 0px auto; + border-bottom: 15px solid rgb(251, 238, 213); + border-left: 15px solid transparent; + border-right: 15px solid transparent; + float: right; + } + &.alert { + display: block; + margin-bottom: 0px; + } +} diff --git a/app/assets/stylesheets/components/_flash-message.scss b/app/assets/stylesheets/components/_flash-message.scss index e6abce7c..f38d59d8 100644 --- a/app/assets/stylesheets/components/_flash-message.scss +++ b/app/assets/stylesheets/components/_flash-message.scss @@ -8,12 +8,41 @@ .message { position: relative; - background: $hb-purple-dark; color: white; font-size: 13px; text-align: center; padding: 8px; margin-bottom: 4px; box-shadow: -1px 2px 2px rgba(0,0,0,0.5); + + .ui-icon-x-thin { + position: relative; + bottom: 5px; + float: right; + font-size: 8px; + margin-left: -16px; + margin-right: -8px; + } + } + + .info { + background: $hb-purple-dark; + } + + .warning { + background: $hb-red; + } + + .progress.message { + background: $navGrey; + box-shadow: -1px 2px 2px rgba($navGrey, 0.5); + color: $hb-purple-dark; + } + + .hb-spinner { + position: relative; + float: right; + right: 28px; + bottom: 4px; } } diff --git a/app/assets/stylesheets/marketing/main.scss b/app/assets/stylesheets/marketing/main.scss index cd0b3325..92a21dab 100644 --- a/app/assets/stylesheets/marketing/main.scss +++ b/app/assets/stylesheets/marketing/main.scss @@ -49,6 +49,7 @@ @import "partials/faq"; @import "partials/integrations"; @import "partials/login"; +@import "partials/permissions"; /** diff --git a/app/assets/stylesheets/marketing/modules/_variables.scss b/app/assets/stylesheets/marketing/modules/_variables.scss index f4df86cd..a26640ee 100644 --- a/app/assets/stylesheets/marketing/modules/_variables.scss +++ b/app/assets/stylesheets/marketing/modules/_variables.scss @@ -15,7 +15,7 @@ $max-width: 1001px; * Breakpoints */ $short: new-breakpoint(max-width 22.5em 4); // 360px -$tall: new-breakpoint(max-width 47.8em 12); // 768px +$tall: new-breakpoint(max-width 52.94em 12); // 847px $grande: new-breakpoint(max-width 65.06em 12); // 1041px $venti: new-breakpoint(min-width 65.06em 12); // 1260px diff --git a/app/assets/stylesheets/marketing/partials/_footer.scss b/app/assets/stylesheets/marketing/partials/_footer.scss index 3d2bbf7b..f14f10ef 100644 --- a/app/assets/stylesheets/marketing/partials/_footer.scss +++ b/app/assets/stylesheets/marketing/partials/_footer.scss @@ -160,7 +160,7 @@ @include e(btn) { @include span-columns(7); - margin-top: 10%; + margin-top: 5%; text-align: center; } @@ -188,7 +188,6 @@ .footer-nav { background: url(asset-path('marketing/separator.png')) no-repeat bottom center; margin: 0 auto; - text-align: left; @include row(); @include rem(( max-width: 500px, diff --git a/app/assets/stylesheets/marketing/partials/_header.scss b/app/assets/stylesheets/marketing/partials/_header.scss index abe71066..39db30e1 100644 --- a/app/assets/stylesheets/marketing/partials/_header.scss +++ b/app/assets/stylesheets/marketing/partials/_header.scss @@ -44,7 +44,7 @@ @include rem(( padding-top: 80px, padding-bottom: 80px, - max-width: $slim-container-width + max-width: 885px )); // Breakpoints @@ -57,6 +57,14 @@ )); } } + + .nav { + .btn-octocat { + position: relative; + right: 6px; + top: 2px; + } + } } diff --git a/app/assets/stylesheets/marketing/partials/_hero.scss b/app/assets/stylesheets/marketing/partials/_hero.scss index c6d1af31..5a3e92fb 100644 --- a/app/assets/stylesheets/marketing/partials/_hero.scss +++ b/app/assets/stylesheets/marketing/partials/_hero.scss @@ -38,6 +38,19 @@ color: white; font-family: $alt-font-family; font-weight: 300; + .nav__btn { + display: inline; + &:before { + display: none; + } + } + .learn-more { + font-size: 26%; + color: white; + padding-left: 2px; + vertical-align: middle; + } + @include rem(( font-size: 52px, margin-bottom: 17px diff --git a/app/assets/stylesheets/marketing/partials/_login.scss b/app/assets/stylesheets/marketing/partials/_login.scss index 65c43cdb..db840ced 100644 --- a/app/assets/stylesheets/marketing/partials/_login.scss +++ b/app/assets/stylesheets/marketing/partials/_login.scss @@ -10,6 +10,14 @@ background-size: 100% auto; color: white; + .permissions { + .btn-octocat { + position: relative; + top: 2px; + margin-right: 4px; + } + } + .top { background: none; } @@ -211,146 +219,3 @@ } -/** - * Permissions - */ -.permissions { - @include row(); - @include rem(( - margin: 0 auto 20px, - max-width: 900px, - )); - - .btn { - display: block; - text-align: center; - } - - /** - * Opening copy - */ - @include e(copy) { - text-align: center; - position: relative; - @include rem(( - font-size: 20px, - margin-bottom: 100px - )); - - &:before { - background: rgba(white, 0.1); - content: " "; - display: block; - height: 3px; - width: 30%; - @include rem(( - margin: 32px auto - )); - } - - // Breakpoints - @include media($tall) { - @include rem(( - margin-bottom: 40px - )); - } - } - - /** - * Column containers - */ - @include e(col) { - @include span-columns(6); - @include rem(( - margin-bottom: 40px - )); - - // Breakpoints - @include media($tall) { - @include span-columns(12); - } - } - @include e(heading) { - line-height: 1; - text-align: center; - @include rem(( - font-size: 32px - )); - - img { - display: inline-block; - vertical-align: middle; - @include rem(( - margin-right: 10px - )); - } - } - - /** - * Permissions details - */ - @include e(list) { - background: white; - border-radius: 5px; - color: #3C3C3C; - @include rem(( - padding: 30px - )); - - dl, dt, dd { - margin: 0; - padding: 0; - } - dl { - border-bottom: 1px solid #E8E8E8; - @include rem(( - padding-bottom: 12px, - margin-bottom: 24px - )); - } - dt { - @include rem(( - line-height: 18px, - margin-bottom: 12px - )); - } - dd { - float: left; - @include rem(( - margin-right: 10px - )); - } - img { - display: inline-block; - @include rem(( - margin-right: 10px - )); - } - - // Breakpoints - @include media($tall) { - dl { - text-align: center; - @include rem(( - padding-bottom: 1px - )); - } - dt { - line-height: 1.5; - @include rem(( - margin-bottom: 24px - )); - } - dd { - float: none; - margin-right: 0; - } - img { - @include rem(( - margin: 0 10px - )); - } - } - } - -} diff --git a/app/assets/stylesheets/marketing/partials/_permissions.scss b/app/assets/stylesheets/marketing/partials/_permissions.scss new file mode 100644 index 00000000..d7fd427b --- /dev/null +++ b/app/assets/stylesheets/marketing/partials/_permissions.scss @@ -0,0 +1,145 @@ +/** + * Permissions + */ +.permissions { + @include row(); + @include rem(( + margin: 0 auto 20px, + max-width: 850px, + )); + + .btn { + display: block; + text-align: center; + margin-bottom: 30px; + } + + /** + * Opening copy + */ + @include e(copy) { + text-align: center; + position: relative; + @include rem(( + font-size: 20px, + margin-bottom: 100px + )); + + &:before { + background: rgba(white, 0.1); + content: " "; + display: block; + height: 3px; + width: 30%; + @include rem(( + margin: 32px auto + )); + } + + // Breakpoints + @include media($tall) { + @include rem(( + margin-bottom: 40px + )); + } + } + + /** + * Column containers + */ + @include e(col) { + @include span-columns(6); + @include rem(( + margin-bottom: 40px + )); + + // Breakpoints + @include media($tall) { + @include span-columns(12); + } + } + @include e(heading) { + line-height: 1; + text-align: center; + @include rem(( + font-size: 32px + )); + + img { + display: inline-block; + vertical-align: middle; + @include rem(( + margin-right: 10px + )); + } + } + + /** + * Permissions details + */ + @include e(list) { + background: white; + border-radius: 5px; + color: #3C3C3C; + @include rem(( + padding: 30px + )); + + dl, dt, dd { + margin: 0; + padding: 0; + } + dl { + border-bottom: 1px solid #E8E8E8; + @include rem(( + padding-bottom: 12px, + margin-bottom: 24px + )); + } + dt { + @include rem(( + line-height: 18px, + margin-bottom: 12px + )); + } + dd { + float: left; + @include rem(( + margin-right: 10px + )); + } + img { + display: inline-block; + @include rem(( + margin-right: 10px + )); + } + + // Breakpoints + @include media($tall) { + dl { + text-align: center; + @include rem(( + padding-bottom: 1px + )); + } + dt { + line-height: 1.5; + @include rem(( + margin-bottom: 24px + )); + } + dd { + float: none; + margin-right: 0; + } + img { + @include rem(( + margin: 0 10px + )); + } + } + } + +} + diff --git a/app/controllers/api/issues_controller.rb b/app/controllers/api/issues_controller.rb index 3de72da0..227409c1 100644 --- a/app/controllers/api/issues_controller.rb +++ b/app/controllers/api/issues_controller.rb @@ -6,6 +6,11 @@ def issue render json: api.issue(params[:number]) end + def issues + api = huboard.board(params[:user], params[:repo]) + render json: api.issues(params[:label], params[:options]) + end + def details api = huboard.board(params[:user], params[:repo]) render json: api.issue(params[:number]).activities @@ -25,6 +30,14 @@ def update_issue def label_issue api = huboard.board(params[:user], params[:repo]) @issue = api.issue(params[:number]).update(params) + @label = params["selectedLabel"] + render json: @issue + end + + def unlabel_issue + api = huboard.board(params[:user], params[:repo]) + @issue = api.issue(params[:number]).update(params) + @label = params["selectedLabel"] render json: @issue end diff --git a/app/controllers/api/webhooks_controller.rb b/app/controllers/api/webhooks_controller.rb index 01f85be1..5a231933 100644 --- a/app/controllers/api/webhooks_controller.rb +++ b/app/controllers/api/webhooks_controller.rb @@ -12,6 +12,42 @@ def legacy render json: { message: "Webhook received" } end + def publish_pull_request_event + return render json: { message: "pong" } if request.env["HTTP_X_GITHUB_EVENT"] == "ping" + + payload = HashWithIndifferentAccess.new JSON.parse(params[:payload]) + return render json: { message: "Fail to parse message" } if payload[:pull_request].nil? + + #guard against the syncronize action + return render json: { message: "Not implemented: `synchronize` event" } if payload[:action] == "synchronize" + + repo = { + repo: { + owner: { login: payload[:repository][:owner][:login] }, + name: payload[:repository][:name], + full_name: payload[:repository][:full_name] + } + } + payload[:pull_request].extend(Huboard::Issues::Card).merge!(repo) + + message = HashWithIndifferentAccess.new( + :pull_request => true, + :issue => payload[:pull_request], + :label => payload[:label], + "action_controller.params" => {}, + :current_user => payload[:sender] + ) + + is_column = Huboard.column_pattern + if payload[:label] && !!payload[:label][:name].match(is_column) + return render json: { message: "Webhook received" } + end + + generate_issue_event(payload[:action], message) + render json: { message: "Webhook received" } + end + + def publish_issue_event return render json: { message: "pong" } if request.env["HTTP_X_GITHUB_EVENT"] == "ping" @@ -29,6 +65,7 @@ def publish_issue_event message = HashWithIndifferentAccess.new( :issue => payload[:issue], + :label => payload[:label], "action_controller.params" => {}, :current_user => payload[:sender] ) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9c5d54cf..aae4d8c9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,17 +4,25 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception rescue_from ActionController::InvalidAuthenticityToken, :with => :csrf_failed rescue_from Ghee::Unauthorized, :with => :ghee_unauthorized + rescue_from Ghee::NotFound, :with => :ghee_not_found include ApplicationHelper after_action :queue_job protected + def ghee_not_found + render({ json: {error: 'Not Found'}, status: 404 }) + end def ghee_unauthorized - request.env['warden'].logout respond_to do |format| format.json { render json: {error: 'GitHub token is expired'}, status: 422} - format.html { redirect_to '/login' } + format.html do + #only logout if html + request.env['warden'].logout + + redirect_to '/login/github' + end end end def csrf_failed diff --git a/app/controllers/board_controller.rb b/app/controllers/board_controller.rb index a03ce60e..48c95524 100644 --- a/app/controllers/board_controller.rb +++ b/app/controllers/board_controller.rb @@ -4,7 +4,6 @@ def index success do @repo = huboard.repo(params[:user],params[:repo]).fetch @repo[:repo].merge!(is_collaborator: is_collaborator?(@repo[:repo])) - @auth_level = github_authenticated?(:private) ? "private" : "public" render :index, layout: "ember" end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 90ee93c0..91ef0210 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,10 +1,15 @@ class DashboardController < ApplicationController before_action :login, only: :private + before_action :determine_auth_scope + def index + github_authenticate!(:default) unless github_authenticated?(:default) + return redirect_to(welcome_path) unless logged_in? && current_user.has_scope?('read:org') @private = nil @user = gh.users(current_user.login) @repos = huboard.all_repos end + def user user = gh.users(params[:user]).raw not_found unless user.status == 200 @@ -47,4 +52,8 @@ def login github_authenticate! :private end + def determine_auth_scope + @auth_scope_private = logged_in? && current_user.has_scope?(:repo) + end + end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index 34036efd..9450ca23 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,19 +1,37 @@ class LoginController < ApplicationController layout false + def logout - request.env['warden'].logout + warden.logout redirect_to "/" end + def public - github_authenticate! :default + github_authenticate! :public @user = gh.user @emails = @user.emails.all - redirect_to params[:redirect_to] || "/" + redirect_to params[:redirect_to] || "/dashboard" end + def private github_authenticate! :private @user = gh.user @emails = @user.emails.all - redirect_to params[:redirect_to] || "/" + redirect_to params[:redirect_to] || "/dashboard" + end + + def github + github_authenticate! :default + redirect_to params[:redirect_to] || "/dashboard" + end + + def github_callback + warden.authenticate!(scope: session[:scope].to_sym) + end + + private + def warden + env['warden'] end + end diff --git a/app/controllers/welcome_controller.rb b/app/controllers/welcome_controller.rb new file mode 100644 index 00000000..3366068d --- /dev/null +++ b/app/controllers/welcome_controller.rb @@ -0,0 +1,6 @@ +class WelcomeController < ApplicationController + layout false + def index; + redirect_to(dashboard_path) if logged_in? && current_user.has_scope?("read:org") + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8d75f3c8..aa65088c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,16 +1,21 @@ require 'bridge/huboard' module ApplicationHelper + def logged_in? - github_authenticated?(:private) || github_authenticated?(:default) + is_default = github_authenticated?(:default) + is_private = github_authenticated?(:private) + is_public = github_authenticated?(:public) + return is_default || is_private || is_public end + def current_user - github_user(:private) || github_user(:default) || OpenStruct.new + github_user(:default) || github_user(:private) || github_user(:public) || User.new(OpenStruct.new()) end def controller? *controller (controller.include?(params[:controller]) || controller.include?(params[:action])) ? "nav__btn--active nav__item--current": '' end def user_token - current_user ? current_user.token : nil + current_user.token end def github_config { @@ -34,7 +39,9 @@ def couch # Initiates the OAuth flow if not already authenticated for the # # specified scope. def github_authenticate!(scope=:default) + request.session[:scope] = scope request.env['warden'].authenticate!(scope: scope) + request.env['warden'].set_user(request.env['warden'].user(scope), scope: :default) end # Logs out a user if currently logged in for the specified scope. @@ -46,8 +53,9 @@ def github_authenticated?(scope=:default) end def github_user(scope=:default) - request.env['warden'].user(scope) + User.new(request.env['warden'].user(scope)) if request.env['warden'].user(scope) end + def github_session(scope=:default) request.env['warden'].session(scope) if github_authenticated?(scope) end @@ -68,6 +76,11 @@ def markdown(text) def generate_issue_event(action, message) verb = action.present_tense constant = "Api::Issues#{verb.capitalize}IssueJob".constantize - constant.perform_later message + + if Rails.configuration.active_job.queue_adapter == :sucker_punch # sucker_punch doesn't support enqueue + constant.perform_later message + else + constant.set(wait: 1.second).perform_later message + end end end diff --git a/app/jobs/api/issues_assign_issue_job.rb b/app/jobs/api/issues_assign_issue_job.rb index 09b93e5e..bbecb7ea 100644 --- a/app/jobs/api/issues_assign_issue_job.rb +++ b/app/jobs/api/issues_assign_issue_job.rb @@ -2,7 +2,11 @@ module Api class IssuesAssignIssueJob < IssueEventJob include IsPublishable action 'assigned' - timestamp Proc.new { Time.now.utc.iso8601} + timestamp ->(params) { params[:issue]['updated_at'] } + cache_key ->(message) { + "assigned.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}" + } + def payload(params) { issue: params[:issue], diff --git a/app/jobs/api/issues_assign_milestone_job.rb b/app/jobs/api/issues_assign_milestone_job.rb index 778385a2..c679c7c9 100644 --- a/app/jobs/api/issues_assign_milestone_job.rb +++ b/app/jobs/api/issues_assign_milestone_job.rb @@ -2,7 +2,7 @@ module Api class IssuesAssignMilestoneJob < IssueEventJob include IsPublishable action 'milestone_changed' - timestamp Proc.new { Time.now.utc.iso8601} + timestamp ->(params) { params[:issue]['updated_at'] } ## suppress if the milestone didn't change # diff --git a/app/jobs/api/issues_label_issue_job.rb b/app/jobs/api/issues_label_issue_job.rb index 6dc98c5e..5a330914 100644 --- a/app/jobs/api/issues_label_issue_job.rb +++ b/app/jobs/api/issues_label_issue_job.rb @@ -1,11 +1,18 @@ module Api class IssuesLabelIssueJob < IssueEventJob - include IsPublishable timestamp ->(params) { params[:issue]['updated_at'] } action "issue_labeled" + cache_key ->(message) { + label_name = message[:payload][:label] ? message[:payload][:label]['name'] : "" + + "#{message[:meta][:action]}.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}.#{ label_name }" + } def payload(params) - { issue: params[:issue] } + { + issue: params[:issue], + label: params[:label] + } end end end diff --git a/app/jobs/api/issues_open_issue_job.rb b/app/jobs/api/issues_open_issue_job.rb index 629f6623..d5c84595 100644 --- a/app/jobs/api/issues_open_issue_job.rb +++ b/app/jobs/api/issues_open_issue_job.rb @@ -6,6 +6,7 @@ class IssuesOpenIssueJob < IssueEventJob def payload(params) { + pull_request: params[:pull_request], issue: params[:issue] } end diff --git a/app/jobs/api/issues_unassign_issue_job.rb b/app/jobs/api/issues_unassign_issue_job.rb index 054052c1..868a1954 100644 --- a/app/jobs/api/issues_unassign_issue_job.rb +++ b/app/jobs/api/issues_unassign_issue_job.rb @@ -2,7 +2,11 @@ module Api class IssuesUnassignIssueJob < IssueEventJob include IsPublishable action 'unassigned' - timestamp Proc.new { Time.now.utc.iso8601} + timestamp ->(params) { params[:issue]['updated_at'] } + cache_key ->(message) { + "assigned.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}" + } + def payload(params) { issue: params[:issue] diff --git a/app/jobs/api/issues_unlabel_issue_job.rb b/app/jobs/api/issues_unlabel_issue_job.rb index 879ef56e..f0b13589 100644 --- a/app/jobs/api/issues_unlabel_issue_job.rb +++ b/app/jobs/api/issues_unlabel_issue_job.rb @@ -2,9 +2,17 @@ module Api class IssuesUnlabelIssueJob < IssueEventJob timestamp ->(params) { params[:issue]['updated_at'] } action "issue_unlabeled" + cache_key ->(message) { + label_name = message[:payload][:label] ? message[:payload][:label]['name'] : "" + + "#{message[:meta][:action]}.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}.#{ label_name }" + } def payload(params) - { issue: params[:issue] } + { + issue: params[:issue], + label: params[:label] + } end end end diff --git a/app/jobs/cached_job.rb b/app/jobs/cached_job.rb new file mode 100644 index 00000000..b6a0d550 --- /dev/null +++ b/app/jobs/cached_job.rb @@ -0,0 +1,27 @@ +class CachedJob < ActiveJob::Base + def perform(params) + Rails.cache.with do |dalli| + uuid = SecureRandom.uuid + if params[:cache_key] && !dalli.get(params[:cache_key]) + dalli.set(params[:cache_key], uuid) + end + + if dalli.get(params[:cache_key]) === uuid + message = deliver params + + return if params[:suppress] + PublishWebhookJob.perform_later message if params[:webhook_publishable] + end + end + end + + :private + + def deliver(message) + client = ::Faye::Redis::Publisher.new({}) + Rails.logger.debug ["/" + message[:meta][:repo_full_name], message] + channel = message[:meta][:repo_full_name].downcase + client.publish "/" + channel, message + return message + end +end diff --git a/app/jobs/issue_event_job.rb b/app/jobs/issue_event_job.rb index bfca753b..5cc7c554 100644 --- a/app/jobs/issue_event_job.rb +++ b/app/jobs/issue_event_job.rb @@ -1,67 +1,66 @@ class IssueEventJob < ActiveJob::Base + # TODO: DRY this up relative to MilestoneEventJob - around_perform :guard_against_double_events + class_attribute :_action + class_attribute :_cache_key + class_attribute :_timestamp + class_attribute :_identifier def self.action(action) - @_action = action + self._action = action end - @_timestamp = Proc.new { Time.now.utc.iso8601 } - def self.timestamp(override=nil) - if override - @_timestamp = override - else - @_timestamp - end + self._timestamp = Proc.new { Time.now.utc.iso8601 } + + def self.timestamp(override) + self._timestamp = override end + + self._identifier = ->(p){ p['issue']['number'] } + def self.identifier(override=nil) - @_identifier = override + self._identifier = override + end + + + self._cache_key = ->(message) { "#{message[:meta][:action]}.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}" } + + def self.cache_key(override) + self._cache_key = override end def self.build_meta(params) #TODO Fix this hack on remapping params to HWIA params = HashWithIndifferentAccess.new(params) issue = HashWithIndifferentAccess.new(params['issue']) - action = @_action.is_a?(String) ? @_action : @_action.call(params) + action = self._action.is_a?(String) ? self._action : self._action.call(params) + HashWithIndifferentAccess.new( action: action, - timestamp: (@_timestamp || Proc.new{Time.now.utc.iso8601}).call(params), + timestamp: self._timestamp.call(params), correlationId: params['action_controller.params']['correlationId'], user: params['current_user'], - identifier: (@_identifier || ->(p){ p['issue']['number']}).call(params), + identifier: self._identifier.call(params), repo_full_name: "#{issue[:repo][:owner][:login]}/#{issue[:repo][:name]}" ) end - def guard_against_double_events - payload = { meta: self.class.build_meta(self.arguments.first) } - willPublish = true - Rails.cache.with do |dalli| - key = "#{payload[:meta][:action]}.#{payload[:meta][:user]["login"]}.#{payload[:meta][:identifier]}.#{payload[:meta][:timestamp]}" - willPublish = false if dalli.get(key) - dalli.set(key, payload.to_s) - end - yield if willPublish + def build_message(params) + payload = payload(params) + payload['actor'] = self.arguments.first['current_user'] + + message = HashWithIndifferentAccess.new({ + meta: self.class.build_meta(arguments.first), + payload: payload + }) end def perform(params) - message = deliver payload(params) - - return if params[:suppress] - - PublishWebhookJob.perform_later message if self.class.included_modules.include? IsPublishable - end + message = build_message(params) + message[:cache_key] = self._cache_key.call(message) + Rails.logger.debug [self.class, 'ActiveJob:cache_key', message[:cache_key]] - def deliver(payload) - payload['actor'] = self.arguments.first['current_user'] - message = { - meta: self.class.build_meta(arguments.first), - payload: payload - } - client = ::Faye::Redis::Publisher.new({}) - Rails.logger.debug ["/" + message[:meta][:repo_full_name], message] - channel = message[:meta][:repo_full_name].downcase - client.publish "/" + channel, message - return message + message[:webhook_publishable] = self.class.included_modules.include? IsPublishable + CachedJob.perform_later(message) end end diff --git a/app/jobs/milestone_event_job.rb b/app/jobs/milestone_event_job.rb index 6c2c76f7..28b32284 100644 --- a/app/jobs/milestone_event_job.rb +++ b/app/jobs/milestone_event_job.rb @@ -1,62 +1,66 @@ class MilestoneEventJob < ActiveJob::Base + # TODO: DRY this up relative to IssueEventJob - around_perform :guard_against_double_events + class_attribute :_action + class_attribute :_cache_key + class_attribute :_timestamp + class_attribute :_identifier def self.action(action) - @_action = action + self._action = action end - @_timestamp = Proc.new { Time.now.utc.iso8601 } - def self.timestamp(override=nil) - if override - @_timestamp = override - else - @_timestamp - end + self._timestamp = Proc.new { Time.now.utc.iso8601 } + + def self.timestamp(override) + self._timestamp = override end + + self._identifier = ->(p){ p['milestone']['number'] } + def self.identifier(override=nil) - @_identifier = override + self._identifier = override + end + + + self._cache_key = ->(message) { "#{message[:meta][:action]}.#{message[:meta][:user]["login"]}.#{message[:meta][:identifier]}.#{message[:meta][:timestamp]}" } + + def self.cache_key(override) + self._cache_key = override end def self.build_meta(params) - milestone = params['milestone'] - action = @_action.is_a?(String) ? @_action : @_action.call(params) + #TODO Fix this hack on remapping params to HWIA + params = HashWithIndifferentAccess.new(params) + milestone = HashWithIndifferentAccess.new(params['milestone']) + action = self._action.is_a?(String) ? self._action : self._action.call(params) + HashWithIndifferentAccess.new( action: action, - timestamp: (@_timestamp || Proc.new{Time.now.utc.iso8601}).call(params), + timestamp: self._timestamp.call(params), correlationId: params['action_controller.params']['correlationId'], user: params['current_user'], - identifier: (@_identifier || ->(p){ p['milestone']['number']}).call(params), + identifier: self._identifier.call(params), repo_full_name: "#{milestone[:repo][:owner][:login]}/#{milestone[:repo][:name]}" ) end - def guard_against_double_events - payload = { meta: self.class.build_meta(self.arguments.first) } - willPublish = true - Rails.cache.with do |dalli| - key = "#{payload[:meta][:action]}.#{payload[:meta][:user]["login"]}.#{payload[:meta][:identifier]}.#{payload[:meta][:timestamp]}" - willPublish = false if dalli.get(key) - dalli.set(key, payload.to_s) - end - yield if willPublish + def build_message(params) + payload = payload(params) + payload['actor'] = self.arguments.first['current_user'] + + message = HashWithIndifferentAccess.new({ + meta: self.class.build_meta(arguments.first), + payload: payload + }) end def perform(params) - message = deliver payload(params) + message = build_message(params) + message[:cache_key] = self._cache_key.call(message) + Rails.logger.debug [self.class, 'ActiveJob:cache_key', message[:cache_key]] - PublishWebhookJob.perform_later message if self.class.included_modules.include? IsPublishable - end - - def deliver(payload) - message = { - meta: self.class.build_meta(arguments.first), - payload: payload - } - client = ::Faye::Redis::Publisher.new({}) - Rails.logger.debug ["/" + message[:meta][:repo_full_name], message] - channel = message[:meta][:repo_full_name].downcase - client.publish "/" + channel, message - return message + message[:webhook_publishable] = self.class.included_modules.include? IsPublishable + CachedJob.perform_later(message) end end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..b1671095 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,22 @@ +class User + ATTRIBUTES = %w[id login name gravatar_id avatar_url email company site_admin].freeze + + extend Forwardable + + attr_reader :warden + + def initialize(warden_user) + @warden = warden_user + end + + def has_scope?(scope) + (self.scope || "").empty? ? false : self.scope.split(',').include?(scope.to_s) + end + + def_delegators :@warden, :token, :scope, :attribs, :safe_avatar_url + + ATTRIBUTES.each do |name| + def_delegator :@warden, name.to_sym + end + +end diff --git a/app/views/board/index.html.erb b/app/views/board/index.html.erb index 151a409f..9fd502ea 100644 --- a/app/views/board/index.html.erb +++ b/app/views/board/index.html.erb @@ -41,7 +41,6 @@ application.set("repo", Ember.Object.create(<%= @repo.to_hash.to_json.html_safe %>)); <% if logged_in? %> application.set("loggedIn", true); - application.set("authLevel", "<%= @auth_level %>") application.set("currentUser",<%= current_user.attribs.to_json.html_safe %>); <% if Rails.application.config.sockets.socket_backend %> application.set("socketBackend", "<%= Rails.application.config.sockets.socket_backend %>"); diff --git a/app/views/dashboard/_repo_filters.html.erb b/app/views/dashboard/_repo_filters.html.erb index 86eb6731..93ecb051 100644 --- a/app/views/dashboard/_repo_filters.html.erb +++ b/app/views/dashboard/_repo_filters.html.erb @@ -7,9 +7,9 @@ end %>