diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..51685710 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# See https://git-scm.com/docs/gitattributes for more about git attribute files. + +# Mark the database schema as having been generated. +db/schema.rb linguist-generated + +# Mark the yarn lockfile as having been generated. +yarn.lock linguist-generated + +# Mark any vendored files as having been vendored. +vendor/* linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dc5db11b --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# See https://help.github.com/articles/ignoring-files for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile '~/.gitignore_global' + +# Ignore bundler config. +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 +/db/*.sqlite3-* + +# Ignore all logfiles and tempfiles. +/log/* +/tmp/* +!/log/.keep +!/tmp/.keep + +# Ignore pidfiles, but keep the directory. +/tmp/pids/* +!/tmp/pids/ +!/tmp/pids/.keep + +# Ignore uploaded files in development. +/storage/* +!/storage/.keep + +/public/assets +.byebug_history + +# Ignore master key for decrypting credentials and more. +/config/master.key + +/public/packs +/public/packs-test +/node_modules +/yarn-error.log +yarn-debug.log* +.yarn-integrity + +config/settings.local.yml +config/settings/*.local.yml +config/environments/*.local.yml diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..96d9df3d --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,90 @@ +require: + - rubocop-rspec + +AllCops: + Exclude: + - vendor/**/* + - tmp/**/* + - lib/tasks/* + - bin/* + - rgloader/* + - db/* + - config/**/* + - spec/rails_helper.rb + - spec/spec_helper.rb + TargetRubyVersion: 2.7 + +Style/Documentation: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/LineLength: + Enabled: false + +Metrics/MethodLength: + CountComments: false + Max: 40 + Exclude: + - spec/**/*.rb + +Metrics/BlockLength: + Exclude: + - spec/**/*.rb + - config/routes.rb + +Metrics/AbcSize: + Enabled: false + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: comma + +Style/StringLiterals: + Exclude: + - db/schema.rb + +Style/IfUnlessModifier: + Enabled: false + +Lint/AssignmentInCondition: + Enabled: false + +RSpec/DescribedClass: + EnforcedStyle: explicit + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/NestedGroups: + Enabled: false + +RSpec/VerifiedDoubles: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/AnyInstance: + Enabled: false + +RSpec/MessageSpies: + EnforcedStyle: receive + +RSpec/NamedSubject: + Enabled: false + +RSpec/SubjectStub: + Enabled: false + +RSpec/LetSetup: + Enabled: false + +RSpec/InstanceVariable: + Enabled: false + +Lint/UnusedMethodArgument: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..2eb2fe97 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.7.2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..6c10d524 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: ruby +os: linux +dist: xenial + +addons: + postgresql: 9.6 + +rvm: 3.0.1 + +env: + global: + - PATH=/snap/bin:$PATH + +cache: bundler + +jobs: + include: + - stage: "rubocop" + script: bundle exec rubocop + - stage: "rspec" + script: bundle exec rspec + before_install: + - "gem install bundler -v 2.1.4" + before_script: + - "RAILS_ENV=test bundle exec rake db:create" + services: + - redis diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..952395f9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM ruby:3.0.1 + +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash -s && \ + apt-get install -y --no-install-recommends \ + postgresql-client nodejs yarn \ + && rm -rf /var/lib/apt/lists/* + +RUN bundle config --global frozen 1 +RUN bundle config set deployment 'true' +RUN bundle config set without 'development test' + +WORKDIR /app +COPY Gemfile* ./ + +RUN gem install bundler -v '2.1.4' +RUN bundle install + +COPY . . + +EXPOSE 3000 +CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..3488405f --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,17 @@ +FROM ruby:3.0.1 + +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ + curl -sL https://deb.nodesource.com/setup_16.x | bash -s && \ + apt-get install -y --no-install-recommends \ + postgresql-client nodejs yarn \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /srv/app +# COPY Gemfile* ./ +# RUN bundle install +# COPY . . + +EXPOSE 3000 +ENTRYPOINT ["/srv/app/entrypoint.sh"] +CMD ["rails", "server", "-b", "0.0.0.0"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..4424c033 --- /dev/null +++ b/Gemfile @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '3.0.1' + +gem 'bootsnap', require: false +gem 'config' +gem 'devise' +gem 'devise-jwt' +gem 'devise-two-factor' +gem 'doorkeeper' +gem 'jsonapi-serializer' +gem 'kaminari' +gem 'ledermann-rails-settings' +gem 'p4ruby' +gem 'pg' +gem 'puma', '~> 5.0' +gem 'rack-cors' +gem 'rails', '~> 6.1.3', '>= 6.1.3.2' +gem 'redis' +gem 'sentry-rails' +gem 'sentry-ruby' +gem 'sentry-sidekiq' +gem 'sidekiq' +gem 'validate_url' + +group :development, :test do + gem 'brakeman' + gem 'byebug', platforms: %i[mri mingw x64_mingw] + gem 'factory_bot' + gem 'listen' + gem 'rspec-rails' +end + +group :test do + gem 'database_cleaner' + gem 'rspec' +end + +group :development do + gem 'rubocop', '~> 0.75.1', require: false + gem 'rubocop-rspec' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..95895bb2 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,368 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + mail (>= 2.7.1) + actionmailer (6.1.3.2) + actionpack (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activesupport (= 6.1.3.2) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (6.1.3.2) + actionview (= 6.1.3.2) + activesupport (= 6.1.3.2) + rack (~> 2.0, >= 2.0.9) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.1.3.2) + actionpack (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + nokogiri (>= 1.8.5) + actionview (6.1.3.2) + activesupport (= 6.1.3.2) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.1.3.2) + activesupport (= 6.1.3.2) + globalid (>= 0.3.6) + activemodel (6.1.3.2) + activesupport (= 6.1.3.2) + activerecord (6.1.3.2) + activemodel (= 6.1.3.2) + activesupport (= 6.1.3.2) + activestorage (6.1.3.2) + actionpack (= 6.1.3.2) + activejob (= 6.1.3.2) + activerecord (= 6.1.3.2) + activesupport (= 6.1.3.2) + marcel (~> 1.0.0) + mini_mime (~> 1.0.2) + activesupport (6.1.3.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + ast (2.4.2) + attr_encrypted (3.1.0) + encryptor (~> 3.0.0) + bcrypt (3.1.16) + bootsnap (1.7.5) + msgpack (~> 1.0) + brakeman (5.0.4) + builder (3.2.4) + byebug (11.1.3) + concurrent-ruby (1.1.9) + config (3.1.0) + deep_merge (~> 1.2, >= 1.2.1) + dry-validation (~> 1.0, >= 1.0.0) + connection_pool (2.2.5) + crass (1.0.6) + database_cleaner (2.0.1) + database_cleaner-active_record (~> 2.0.0) + database_cleaner-active_record (2.0.1) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) + deep_merge (1.2.1) + devise (4.8.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0) + responders + warden (~> 1.2.3) + devise-jwt (0.8.1) + devise (~> 4.0) + warden-jwt_auth (~> 0.5) + devise-two-factor (4.0.0) + activesupport (< 6.2) + attr_encrypted (>= 1.3, < 4, != 2) + devise (~> 4.0) + railties (< 6.2) + rotp (~> 6.0) + diff-lcs (1.4.4) + doorkeeper (5.5.2) + railties (>= 5) + dry-auto_inject (0.8.0) + dry-container (>= 0.3.4) + dry-configurable (0.12.1) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5.0) + dry-container (0.8.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.6.0) + concurrent-ruby (~> 1.0) + dry-equalizer (0.3.0) + dry-inflector (0.2.0) + dry-initializer (3.0.4) + dry-logic (1.2.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.5, >= 0.5) + dry-schema (1.6.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.8, >= 0.8.3) + dry-core (~> 0.5, >= 0.5) + dry-initializer (~> 3.0) + dry-logic (~> 1.0) + dry-types (~> 1.5) + dry-types (1.5.1) + concurrent-ruby (~> 1.0) + dry-container (~> 0.3) + dry-core (~> 0.5, >= 0.5) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) + dry-validation (1.6.0) + concurrent-ruby (~> 1.0) + dry-container (~> 0.7, >= 0.7.1) + dry-core (~> 0.4) + dry-equalizer (~> 0.2) + dry-initializer (~> 3.0) + dry-schema (~> 1.5, >= 1.5.2) + encryptor (3.0.0) + erubi (1.10.0) + factory_bot (6.2.0) + activesupport (>= 5.0.0) + faraday (1.8.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + ffi (1.15.1) + globalid (0.4.2) + activesupport (>= 4.2.0) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + jaro_winkler (1.5.4) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) + jwt (2.2.3) + kaminari (1.2.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) + actionview + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) + activerecord + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) + ledermann-rails-settings (2.5.0) + activerecord (>= 4.2) + listen (3.5.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + loofah (2.10.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.1) + method_source (1.0.0) + mini_mime (1.0.3) + mini_portile2 (2.5.3) + minitest (5.14.4) + msgpack (1.4.2) + multipart-post (2.1.1) + nio4r (2.5.7) + nokogiri (1.11.7) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + orm_adapter (0.5.0) + p4ruby (2020.1.2056123) + parallel (1.20.1) + parser (3.0.1.1) + ast (~> 2.4.1) + pg (1.2.3) + public_suffix (4.0.6) + puma (5.3.2) + nio4r (~> 2.0) + racc (1.5.2) + rack (2.2.3) + rack-cors (1.1.1) + rack (>= 2.0.0) + rack-test (1.1.0) + rack (>= 1.0, < 3) + rails (6.1.3.2) + actioncable (= 6.1.3.2) + actionmailbox (= 6.1.3.2) + actionmailer (= 6.1.3.2) + actionpack (= 6.1.3.2) + actiontext (= 6.1.3.2) + actionview (= 6.1.3.2) + activejob (= 6.1.3.2) + activemodel (= 6.1.3.2) + activerecord (= 6.1.3.2) + activestorage (= 6.1.3.2) + activesupport (= 6.1.3.2) + bundler (>= 1.15.0) + railties (= 6.1.3.2) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.3.0) + loofah (~> 2.3) + railties (6.1.3.2) + actionpack (= 6.1.3.2) + activesupport (= 6.1.3.2) + method_source + rake (>= 0.8.7) + thor (~> 1.0) + rainbow (3.0.0) + rake (13.0.3) + rb-fsevent (0.11.0) + rb-inotify (0.10.1) + ffi (~> 1.0) + redis (4.3.1) + responders (3.0.1) + actionpack (>= 5.0) + railties (>= 5.0) + rotp (6.2.0) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-rails (5.0.1) + actionpack (>= 5.2) + activesupport (>= 5.2) + railties (>= 5.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.10.2) + rubocop (0.75.1) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.6) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 1.7) + rubocop-rspec (1.41.0) + rubocop (>= 0.68.1) + ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) + sentry-rails (4.7.2) + railties (>= 5.0) + sentry-ruby-core (~> 4.7.0) + sentry-ruby (4.7.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + faraday (>= 1.0) + sentry-ruby-core (= 4.7.2) + sentry-ruby-core (4.7.2) + concurrent-ruby + faraday + sentry-sidekiq (4.7.2) + sentry-ruby-core (~> 4.7.0) + sidekiq (>= 3.0) + sidekiq (6.2.1) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) + sprockets (4.0.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.2) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (1.1.0) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + unicode-display_width (1.6.1) + validate_url (1.0.13) + activemodel (>= 3.0.0) + public_suffix + warden (1.2.9) + rack (>= 2.0.9) + warden-jwt_auth (0.5.0) + dry-auto_inject (~> 0.6) + dry-configurable (~> 0.9) + jwt (~> 2.1) + warden (~> 1.2) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.4.2) + +PLATFORMS + ruby + +DEPENDENCIES + bootsnap + brakeman + byebug + config + database_cleaner + devise + devise-jwt + devise-two-factor + doorkeeper + factory_bot + jsonapi-serializer + kaminari + ledermann-rails-settings + listen + p4ruby + pg + puma (~> 5.0) + rack-cors + rails (~> 6.1.3, >= 6.1.3.2) + redis + rspec + rspec-rails + rubocop (~> 0.75.1) + rubocop-rspec + sentry-rails + sentry-ruby + sentry-sidekiq + sidekiq + validate_url + +RUBY VERSION + ruby 3.0.1p64 + +BUNDLED WITH + 2.2.15 diff --git a/README.md b/README.md index 6a524a0f..1760ea77 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ # travis-vcs-proxy -A Travis CI Proxy backend for integration wit VCS other than Git +A Travis CI Proxy backend for integration with VCS other than Git diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..3bf36a4a --- /dev/null +++ b/Rakefile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks + +unless ENV['RACK_ENV'] == 'production' + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) + + require 'rubocop/rake_task' + RuboCop::RakeTask.new do |t| + t.patterns = ['{app,spec}/**/*.rb', '{Rake,Gem}file', 'config.ru'] + end + + task default: %i[spec rubocop] +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 00000000..a3cff5b8 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ApplicationController < ActionController::API + before_action :require_2fa + + private + + def require_authentication + head :unauthorized && return unless user_signed_in? + end + + def require_2fa + head :unauthorized && return unless two_factor_auth_enabled? + end + + def two_factor_auth_enabled? + !user_signed_in? || current_user.otp_required_for_login? + end + + def presented_entity(resource_name, resource) + "#{resource_name.to_s.classify}Serializer".constantize.new(resource, params: { current_user: current_user }).to_h + end + + def current_user_jwt_token + request.env[Warden::JWTAuth::Hooks::PREPARED_TOKEN_ENV_KEY] + end + + def user_signed_in? + super || current_resource_owner.present? + end + + def current_user + super || current_resource_owner + end + + def current_resource_owner + return @current_resource_owner if defined?(@current_resource_owner) + + unless valid_doorkeeper_token? + @current_resource_owner = nil + return + end + + @current_resource_owner = User.find(doorkeeper_token.resource_owner_id) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb new file mode 100644 index 00000000..87d48c4e --- /dev/null +++ b/app/controllers/concerns/paginated_collection.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module PaginatedCollection + extend ActiveSupport::Concern + + def paginated_collection(resource_name, serializer, collection) + data = {} + limit = collection.limit_value + pagination = { + count: collection.total_count, + is_first: collection.first_page?, + is_last: collection.last_page?, + offset: (collection.current_page - 1) * limit, + limit: limit, + first: { + limit: limit, + offset: 0, + }, + last: { + limit: limit, + offset: (collection.total_pages - 1) * limit, + }, + prev: nil, + next: nil, + } + pagination[:prev] = { limit: limit, offset: (collection.prev_page - 1) * limit } if collection.prev_page + pagination[:next] = { limit: limit, offset: collection.current_page * limit } if collection.next_page + + data[:meta] = { pagination: pagination } + data[resource_name] = collection.map do |item| + presented_entity(serializer, item) + end + + data + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 00000000..ce8a95e8 --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class HomeController < ApplicationController + def index + head :ok + end +end diff --git a/app/controllers/v1/repositories/branches_controller.rb b/app/controllers/v1/repositories/branches_controller.rb new file mode 100644 index 00000000..fe95c06f --- /dev/null +++ b/app/controllers/v1/repositories/branches_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module V1 + module Repositories + class BranchesController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:show] + + def index + render json: @repository.branches.map { |branch| presented_entity(:ref, branch) } + end + + def show + render json: presented_entity(:ref, @branch) + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_branch + @branch = @repository.branches.find(params[:id]) + end + end + end +end diff --git a/app/controllers/v1/repositories/commits_controller.rb b/app/controllers/v1/repositories/commits_controller.rb new file mode 100644 index 00000000..4588435b --- /dev/null +++ b/app/controllers/v1/repositories/commits_controller.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module V1 + module Repositories + class CommitsController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_branch, only: [:index] + before_action :set_commit, only: [:show] + + def index + render json: @branch.commits.order('committed_at DESC').map { |commit| presented_entity(:commit, commit) } + end + + def show + render json: presented_entity(:commit, @commit) + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_branch + @branch = @repository.branches.find_by!(name: params[:branch]) + end + + def set_commit + @commit = @repository.commits.find_by!(sha: params[:id]) + end + end + end +end diff --git a/app/controllers/v1/repositories/token_controller.rb b/app/controllers/v1/repositories/token_controller.rb new file mode 100644 index 00000000..13543cac --- /dev/null +++ b/app/controllers/v1/repositories/token_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module V1 + module Repositories + class TokenController < ApplicationController + before_action :require_authentication + before_action :set_repository + + def get + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) + + render json: { token: @repository.decrypted_token(@repository.server_provider.settings(:p4_host).token) } + end + + def update # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:bad_request) && return if params[:username].blank? || params[:token].blank? + + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) + + success = false + begin + success = UpdateRepositoryCredentials.new(@repository, params[:username], params[:token]).call + rescue UpdateRepositoryCredentials::ValidationFailed + render(json: { errors: ['Cannot authenticate'] }, status: :unprocessable_entity) && (return) + end + + head(:ok) && return if success + + render json: { errors: @repository.errors }, status: :unprocessable_entity + end + + def destroy # rubocop:disable Metrics/CyclomaticComplexity + permission = current_user.repository_permission(@repository.id) + head(:forbidden) && return if permission.blank? || (!permission.owner? && !permission.admin?) + + head(:ok) && return if UpdateRepositoryCredentials.new(@repository, nil, nil).call + + render json: { errors: @repository.errors }, status: :unprocessable_entity + end + + private + + def set_repository + @repository = current_user.repositories.find_by(id: params[:repository_id]) + end + end + end +end diff --git a/app/controllers/v1/repositories/webhooks_controller.rb b/app/controllers/v1/repositories/webhooks_controller.rb new file mode 100644 index 00000000..de88e5d6 --- /dev/null +++ b/app/controllers/v1/repositories/webhooks_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module V1 + module Repositories + class WebhooksController < ApplicationController + before_action :require_authentication + before_action :set_repository + before_action :set_webhook, only: %i[show update] + + def index + render json: @repository.webhooks.map { |webhook| presented_entity(:webhook, webhook) } + end + + def show + render json: presented_entity(:webhook, @webhook) + end + + def create + webhook = @repository.webhooks.build(webhook_params) + if webhook.save + render json: presented_entity(:webhook, webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + def update + if @webhook.update(webhook_params) + render json: presented_entity(:webhook, @webhook) + return + end + + render json: { errors: webhook.errors.full_messages }, status: :unprocessable_entity + end + + private + + def set_repository + @repository = current_user.repositories.find(params[:repository_id]) + end + + def set_webhook + @webhook = @repository.webhooks.find(params[:id]) + end + + def webhook_params + params.require(:webhook).permit(:name, :url, :active, :insecure_ssl) + end + end + end +end diff --git a/app/controllers/v1/repositories_controller.rb b/app/controllers/v1/repositories_controller.rb new file mode 100644 index 00000000..fdd3d508 --- /dev/null +++ b/app/controllers/v1/repositories_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module V1 + class RepositoriesController < ApplicationController + before_action :require_authentication + before_action :set_repository + + def show + render json: presented_entity(:repository, @repository) + end + + def refs + render json: @repository.refs.map { |ref| presented_entity(:full_ref, ref) } + end + + def content + head(:bad_request) && return if params[:ref].blank? || params[:path].blank? + + result = @repository.file_contents(params[:ref], params[:path]) + render(json: { errors: ['Cannot render file'] }, status: :unprocessable_entity) && return if result.blank? + + render plain: result[1] + end + + def sync + SyncJob.perform_later(SyncJob::SyncType::REPOSITORY, @repository.id, current_user.id) + + head :ok + end + + private + + def set_repository + @repository = current_user.repositories.includes(:permissions).find(params[:id]) + end + end +end diff --git a/app/controllers/v1/server_providers_controller.rb b/app/controllers/v1/server_providers_controller.rb new file mode 100644 index 00000000..0afde728 --- /dev/null +++ b/app/controllers/v1/server_providers_controller.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +module V1 + class ServerProvidersController < ApplicationController + include PaginatedCollection + + before_action :require_authentication + before_action :set_server_provider, only: %i[show update authenticate forget repositories sync] + + PROVIDER_KLASS = { + 'perforce' => P4ServerProvider, + }.freeze + + def create # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if params[:server_provider].blank? || !PROVIDER_KLASS.key?(params[:server_provider][:type]) + head(:bad_request) && return + end + + klass = PROVIDER_KLASS[params[:server_provider][:type]] + if klass.find_by(url: params[:server_provider][:url]).present? + render(json: { errors: ['A server with this URL already exists.'] }, status: :unprocessable_entity) && return + end + + errors = [] + provider = nil + ActiveRecord::Base.transaction do + provider = klass.new(server_provider_params) + unless provider.save + errors = provider.errors + raise ActiveRecord::Rollback + end + + set_provider_credentials(provider, errors) + + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:owner]) + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end + end + + render(json: presented_entity(:server_provider, provider)) && return if errors.blank? + + render json: { errors: errors }, status: :unprocessable_entity + end + + def show + render json: presented_entity(:server_provider, @server_provider) + end + + def update # rubocop:disable Metrics/CyclomaticComplexity + permission = current_user.server_provider_permission(@server_provider.id) + head(:forbidden) && return if permission.blank? || !permission.owner? + + errors = [] + ActiveRecord::Base.transaction do + unless @server_provider.update(server_provider_params) + errors = @server_provider.errors + raise ActiveRecord::Rollback + end + + set_provider_credentials(@server_provider, errors) + + setting = permission.setting || permission.build_setting + setting.token = params[:server_provider][:token] + setting.username = params[:server_provider][:username] + setting.save + + end + + head(:ok) && return if errors.blank? + + render json: { errors: errors }, status: :unprocessable_entity + end + + def authenticate # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:bad_request) && return if params[:token].blank? || params[:username].blank? + + success = true + ActiveRecord::Base.transaction do + permission = current_user.server_provider_permissions.find_or_initialize_by(server_provider_id: @server_provider.id) + unless permission.persisted? + permission.permission = ServerProviderPermission.permissions[:member] + unless permission.save + success = false + raise ActiveRecord::Rollback + end + end + + unless AuthenticateUserWithServerProvider.new(permission, @server_provider, params[:username], params[:token]).call + success = false + raise ActiveRecord::Rollback + end + end + + head(:ok) && return if success + + render json: { errors: ['Cannot authenticate'] }, status: :unprocessable_entity + end + + def forget + current_user.server_provider_permission(@server_provider.id)&.destroy + + head :ok + end + + def sync + SyncJob.perform_later(SyncJob::SyncType::SERVER_PROVIDER, @server_provider.id, current_user.id) + + head :ok + end + + def repositories + order = params[:sort_by] == 'last_synced_at' ? 'DESC' : 'ASC' + repositories = @server_provider.repositories + .includes(:permissions) + .includes(:setting_objects) + repositories = repositories.order(params[:sort_by] => order) if params[:sort_by].present? + repositories = repositories.where('name LIKE ?', "%#{params[:filter]}%") if params[:filter].present? + + render json: paginated_collection(:repositories, :repository, repositories.page(params[:page]).per(params[:limit])) + end + + def by_url + head(:bad_request) && return if params[:url].blank? + + render json: presented_entity(:server_provider, ServerProvider.find_by!(url: params[:url])) + end + + def add_by_url + head(:bad_request) && return if params[:url].blank? + + errors = [] + provider = ServerProvider.find_by!(url: params[:url]) + + unless current_user.set_server_provider_permission(provider.id, ServerProviderPermission.permissions[:member]) + errors << 'Cannot set permission for user' + raise ActiveRecord::Rollback + end + + render(json: presented_entity(:server_provider, provider)) && return if errors.blank? + + render json: { errors: errors }, status: :unprocessable_entity + end + + private + + def server_provider_params + params.require(:server_provider).permit(:name, :url) + end + + def set_server_provider + @server_provider = ServerProvider.includes(:server_provider_permissions).find(params[:id]) + end + + def set_provider_credentials(provider, errors) + return if params[:server_provider][:username].blank? || params[:server_provider][:token].blank? + + success = false + begin + success = UpdateRepositoryCredentials.new(provider, params[:server_provider][:username], params[:server_provider][:token]).call + rescue UpdateRepositoryCredentials::ValidationFailed + errors << 'Cannot authenticate' + raise ActiveRecord::Rollback + end + + return if success + + errors << 'Cannot save credentials' + raise ActiveRecord::Rollback + end + end +end diff --git a/app/controllers/v1/users/confirmations_controller.rb b/app/controllers/v1/users/confirmations_controller.rb new file mode 100644 index 00000000..6823bbd1 --- /dev/null +++ b/app/controllers/v1/users/confirmations_controller.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module V1 + module Users + class ConfirmationsController < Devise::ConfirmationsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa + + def show + user = User.confirm_by_token(params[:confirmation_token]) + if user.errors.present? + redirect_uri = URI.join(Settings.web_url, 'unconfirmed') + redirect_uri.query = 'error=expired' + redirect_to(redirect_uri.to_s) && return + end + + redirect_to URI.join(Settings.web_url, 'confirmed').to_s + end + + def resend + head(:bad_request) && return if params[:email].blank? + + user = User.find_by(email: params[:email]) + head(:ok) && return if user.blank? || user.confirmed? + + user.resend_confirmation_instructions + + head :ok + end + end + end +end diff --git a/app/controllers/v1/users/registrations_controller.rb b/app/controllers/v1/users/registrations_controller.rb new file mode 100644 index 00000000..920ad9ec --- /dev/null +++ b/app/controllers/v1/users/registrations_controller.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module V1 + module Users + class RegistrationsController < Devise::RegistrationsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa + + def create + build_resource(sign_up_params) + + resource.save + if resource.persisted? + expire_data_after_sign_in! + head :ok + return + end + + render json: { errors: resource.errors }, status: :unprocessable_entity + end + + def update + head :not_found + end + + def destroy + unless resource.valid_password?(params[:password]) + render(json: { errors: ['Invalid credentials'] }, status: :unprocessable_entity) && return + end + + if params[:feedback].present? + permitted = params.require(:feedback).permit(:reason, :text) + FeedbackMailer.with(email: resource.email, feedback: permitted).send_feedback.deliver_now + end + + resource.mark_as_deleted + sign_out + head :ok + end + + def cancel + head :not_found + end + end + end +end diff --git a/app/controllers/v1/users/sessions_controller.rb b/app/controllers/v1/users/sessions_controller.rb new file mode 100644 index 00000000..f25924ec --- /dev/null +++ b/app/controllers/v1/users/sessions_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module V1 + module Users + class SessionsController < Devise::SessionsController + clear_respond_to + respond_to :json + skip_before_action :require_2fa + + def create # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if !params.key?(:user) || params[:user][:email].blank? || params[:user][:password].blank? + head(:bad_request) && return + end + user = User.find_by(email: params[:user][:email]) + head(:unauthorized) && return if user.blank? + if user.valid_password?(params[:user][:password]) && user.otp_required_for_login? && params[:user][:otp_attempt].blank? + render(json: { token: '', otp_enabled: true }) && return + end + + user = warden.authenticate!(auth_options) + + render json: { token: current_user_jwt_token, otp_enabled: user.otp_required_for_login? } + end + + private + + # Overriding because we don't need to send any answer + def respond_to_on_destroy + head :ok + end + + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt]) + end + end + end +end diff --git a/app/controllers/v1/users/two_factor_auth_controller.rb b/app/controllers/v1/users/two_factor_auth_controller.rb new file mode 100644 index 00000000..bfeef27e --- /dev/null +++ b/app/controllers/v1/users/two_factor_auth_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module V1 + module Users + class TwoFactorAuthController < ApplicationController + before_action :require_authentication + skip_before_action :require_2fa, only: %i[url enable] + + def url + if current_user.otp_secret.blank? + current_user.otp_secret = User.generate_otp_secret + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save + end + + render json: { url: current_user.otp_provisioning_uri(current_user.email, issuer: 'Travis CI VCS Proxy') } + end + + def enable + unless params[:otp_attempt] == current_user.current_otp + render(json: { errors: ['Wrong OTP code'] }, status: :unprocessable_entity) && return + end + + current_user.otp_required_for_login = true + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save + + User.revoke_jwt(nil, current_user) + warden.set_user(current_user) + render json: { token: current_user_jwt_token, otp_enabled: true } + end + + def codes + codes = current_user.generate_otp_backup_codes! + render(json: { errors: current_user.errors }, status: :unprocessable_entity) && return unless current_user.save + + render json: { codes: codes } + end + end + end +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb new file mode 100644 index 00000000..9bd16016 --- /dev/null +++ b/app/controllers/v1/users_controller.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +module V1 + class UsersController < ApplicationController + include PaginatedCollection + + before_action :require_authentication, except: %i[request_password_reset reset_password] + skip_before_action :require_2fa, only: %i[show request_password_reset reset_password] + + def show + render json: presented_entity(:user, current_user) + end + + def update_email + head(:bad_request) && return if params[:email].blank? + + current_user.email = params[:email] + if current_user.save + head :ok + return + end + + render json: { errors: current_user.errors }, status: :unprocessable_entity + end + + def update_password # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + if params[:current_password].blank? || params[:password].blank? || params[:password_confirmation].blank? + head(:bad_request) && return + end + + unless current_user.valid_password?(params[:current_password]) + render json: { errors: ['Invalid current password'] }, status: :unprocessable_entity + return + end + + unless params[:password] == params[:password_confirmation] + render json: { errors: ['Password does not match confirmation'] }, status: :unprocessable_entity + return + end + + current_user.password = params[:password] + current_user.password_confirmation = params[:password_confirmation] + if current_user.save + head :ok + return + end + + render json: { errors: current_user.errors }, status: :unprocessable_entity + end + + def request_password_reset + head(:ok) && return if params[:email].blank? + head(:ok) && return unless user = User.find_by(email: params[:email]) + + user.send_reset_password_instructions + + head :ok + end + + def reset_password + if params[:reset_password_token].blank? || params[:password].blank? || params[:password_confirmation].blank? + head(:bad_request) && return + end + + user = User.reset_password_by_token(params.slice(:reset_password_token, :password, :password_confirmation)) + if user.errors.empty? + head :ok + return + end + + render json: { errors: user.errors }, status: :unprocessable_entity + end + + def emails + render json: { emails: [current_user.email] } + end + + def server_providers + server_providers = current_user.server_providers + .includes(:server_provider_permissions) + .includes(:setting_objects) + .order(:name) + .page(params[:page]) + .per(params[:limit]) + + render json: paginated_collection(:server_providers, :server_provider, server_providers) + end + + def repositories + render json: current_user.repositories.map { |repository| presented_entity(:repository, repository) } + end + + def sync + SyncJob.perform_later(SyncJob::SyncType::USER, current_user.id) + + head :ok + end + end +end diff --git a/app/controllers/v1/webhooks_controller.rb b/app/controllers/v1/webhooks_controller.rb new file mode 100644 index 00000000..1f786854 --- /dev/null +++ b/app/controllers/v1/webhooks_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module V1 + class WebhooksController < ApplicationController + def receive # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + head(:unauthorized) && return unless server_provider = ServerProvider.find_by(listener_token: params[:token]) + + head(:internal_server_error) && return unless commit_info = server_provider.commit_info_from_webhook(params) + + # TODO: Figure out if we should really ignore the hook if there is no user with the given email + head(:ok) && return unless user = server_provider.users.find_by(email: commit_info[:email]) + # TODO: Figure out if we should really ignore the hook if there is no repository with this name + head(:ok) && return unless repository = server_provider.repositories.find_by(name: commit_info[:repository_name]) + + ref = repository.refs.branch.find_by(name: commit_info[:ref]) || repository.refs.branch.create(name: commit_info[:ref]) + head(:unprocessable_entity) && return unless ref + + commit = ref.commits.find_by(sha: params[:sha]) || ref.commits.create(sha: params[:sha], repository: repository, user: user, committed_at: Time.now) + head(:unprocessable_entity) && return unless commit + + TriggerWebhooks.new(commit).call + + head :ok + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 00000000..15b06f0f --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +module ApplicationHelper +end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js new file mode 100644 index 00000000..0286f291 --- /dev/null +++ b/app/javascript/packs/application.js @@ -0,0 +1,8 @@ +// This file is automatically compiled by Webpack, along with any other files +// present in this directory. You're encouraged to place your actual application logic in +// a relevant structure within app/javascript and only use these pack files to reference +// that code so it'll be compiled. + +import App from '../src/app.module.es6'; + +App.create(); diff --git a/app/javascript/src/adapters/.gitkeep b/app/javascript/src/adapters/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/adapters/application.js b/app/javascript/src/adapters/application.js new file mode 100644 index 00000000..50b375f0 --- /dev/null +++ b/app/javascript/src/adapters/application.js @@ -0,0 +1,4 @@ +// Override the default adapter with the `DS.ActiveModelAdapter` which +// is built to work nicely with the ActiveModel::Serializers gem. +App.ApplicationAdapter = DS.ActiveModelAdapter.extend({ +}); diff --git a/app/javascript/src/app.js b/app/javascript/src/app.js new file mode 100644 index 00000000..b5af479f --- /dev/null +++ b/app/javascript/src/app.js @@ -0,0 +1,11 @@ +//= require_tree ./adapters +//= require_tree ./mixins +//= require_tree ./models +//= require_tree ./controllers +//= require_tree ./views +//= require_tree ./helpers +//= require_tree ./components +//= require_tree ./templates +//= require_tree ./routes +//= require ./router +//= require_self diff --git a/app/javascript/src/application.js b/app/javascript/src/application.js new file mode 100644 index 00000000..960c9c85 --- /dev/null +++ b/app/javascript/src/application.js @@ -0,0 +1,12 @@ +//= require jquery +//= require jquery_ujs +//= require ./environment +//= require ember +//= require ember-data +//= require active-model-adapter + +//= require_self +//= require ./app + +// for more details see: http://emberjs.com/guides/application/ +App = Ember.Application.create(); diff --git a/app/javascript/src/components/.gitkeep b/app/javascript/src/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/controllers/.gitkeep b/app/javascript/src/controllers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/environment.js b/app/javascript/src/environment.js new file mode 100644 index 00000000..3b0b6ca2 --- /dev/null +++ b/app/javascript/src/environment.js @@ -0,0 +1,7 @@ +window.EmberENV = { + // FEATURES: {}, + // EXTEND_PROTOTYPES: { + // Function: false, + // Array: true + // } +}; diff --git a/app/javascript/src/helpers/.gitkeep b/app/javascript/src/helpers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/initializers/.gitkeep b/app/javascript/src/initializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/instance-initializers/.gitkeep b/app/javascript/src/instance-initializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/mixins/.gitkeep b/app/javascript/src/mixins/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/models/.gitkeep b/app/javascript/src/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/router.js b/app/javascript/src/router.js new file mode 100644 index 00000000..0f89a424 --- /dev/null +++ b/app/javascript/src/router.js @@ -0,0 +1,5 @@ +// For more information see: http://emberjs.com/guides/routing/ + +App.Router.map(function() { + // this.resource('posts'); +}); diff --git a/app/javascript/src/routes/.gitkeep b/app/javascript/src/routes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/serializers/.gitkeep b/app/javascript/src/serializers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/services/.gitkeep b/app/javascript/src/services/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/templates/.gitkeep b/app/javascript/src/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/templates/components/.gitkeep b/app/javascript/src/templates/components/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/transforms/.gitkeep b/app/javascript/src/transforms/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/javascript/src/views/.gitkeep b/app/javascript/src/views/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..bef39599 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/sync_job.rb b/app/jobs/sync_job.rb new file mode 100644 index 00000000..403a3de3 --- /dev/null +++ b/app/jobs/sync_job.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'travis/vcs_proxy/syncer' + +class SyncJob < ApplicationJob + class SyncType + SERVER_PROVIDER = 1 + REPOSITORY = 2 + USER = 3 + end + + queue_as :default + + def perform(sync_type, id, user_id = nil) + user = user_id.present? ? User.find(user_id) : User.find(id) + syncer = Travis::VcsProxy::Syncer.new(user) + case sync_type + when SyncType::SERVER_PROVIDER then syncer.sync_server_provider(ServerProvider.find(id)) + when SyncType::REPOSITORY then syncer.sync_repository(Repository.find(id)) + when SyncType::USER then syncer.sync_user + end + end +end diff --git a/app/jobs/webhook_trigger_job.rb b/app/jobs/webhook_trigger_job.rb new file mode 100644 index 00000000..0ee41bc9 --- /dev/null +++ b/app/jobs/webhook_trigger_job.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class WebhookTriggerJob < ApplicationJob + queue_as :default + + def perform(commit_id) + commit = Commit.find(commit_id) + + TriggerWebhooks.new(commit).call + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 00000000..04bbea05 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base + default from: Settings.mail_from + layout 'mailer' +end diff --git a/app/mailers/feedback_mailer.rb b/app/mailers/feedback_mailer.rb new file mode 100644 index 00000000..518c1e2c --- /dev/null +++ b/app/mailers/feedback_mailer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class FeedbackMailer < ApplicationMailer + default from: 'from@example.com' + layout 'mailer' + + def send_feedback + @email = params[:email] + @feedback = params[:feedback] + mail(to: Settings.feedback_mail, subject: 'Account removed') + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..71fbba5b --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/commit.rb b/app/models/commit.rb new file mode 100644 index 00000000..25d2380e --- /dev/null +++ b/app/models/commit.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class Commit < ApplicationRecord + belongs_to :ref + belongs_to :repository + belongs_to :user + + validates_presence_of :ref_id, :repository_id, :user_id, :sha +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/concerns/encrypted_token.rb b/app/models/concerns/encrypted_token.rb new file mode 100644 index 00000000..41a7a131 --- /dev/null +++ b/app/models/concerns/encrypted_token.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module EncryptedToken + extend ActiveSupport::Concern + + def encrypted_token(token_value) + encryptor.encrypt_and_sign(token_value) + end + + def decrypted_token(token_value) + encryptor.decrypt_and_verify(token_value) + end + + private + + def encryptor + @encryptor ||= ActiveSupport::MessageEncryptor.new(Settings.p4_token_encryption_key) + end +end diff --git a/app/models/concerns/p4_host_settings.rb b/app/models/concerns/p4_host_settings.rb new file mode 100644 index 00000000..d7044aa4 --- /dev/null +++ b/app/models/concerns/p4_host_settings.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module P4HostSettings + extend ActiveSupport::Concern + + included do + has_settings(persistent: true) do |s| + s.key :p4_host, defaults: { username: '', token: '' } + end + + def token=(token_value) + settings(:p4_host).token = token_value.blank? ? token_value : encrypted_token(token_value) + end + + def token + tok = settings(:p4_host).token + return tok if tok.blank? + + decrypted_token(tok) + end + end +end diff --git a/app/models/p4_server_provider.rb b/app/models/p4_server_provider.rb new file mode 100644 index 00000000..02a47ac5 --- /dev/null +++ b/app/models/p4_server_provider.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class P4ServerProvider < ServerProvider + include P4HostSettings + include EncryptedToken + + def bare_repo(repository = nil, username = nil, password = nil) + if username.present? && password.present? + repo_token = password + elsif repository.present? && repository.settings(:p4_host).username.present? + username = repository.settings(:p4_host).username + repo_token = repository.token + else + username = settings(:p4_host).username + repo_token = token + end + + Travis::VcsProxy::Repositories::P4.new(repository, url, username, repo_token) + end + + def remote_repositories(username = nil, token = nil) + bare_repo(nil, username, token).repositories + end + + def commit_info_from_webhook(payload) + return unless payload.key?(:change_root) && payload.key?(:username) + + bare_repo.commit_info(payload[:change_root], payload[:username]) + end + + def provider_type + 'perforce' + end + + def default_branch + 'master' + end +end diff --git a/app/models/ref.rb b/app/models/ref.rb new file mode 100644 index 00000000..933dbfec --- /dev/null +++ b/app/models/ref.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Ref < ApplicationRecord + enum type: %i[branch tag] + + self.inheritance_column = nil + + belongs_to :repository + + has_many :commits, dependent: :destroy + + validates_presence_of :name, :type, :repository_id + + scope :branch, -> { where(type: types[:branch]) } + scope :tag, -> { where(type: types[:tag]) } +end diff --git a/app/models/repository.rb b/app/models/repository.rb new file mode 100644 index 00000000..07ce19f0 --- /dev/null +++ b/app/models/repository.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Repository < ApplicationRecord + include P4HostSettings + include EncryptedToken + + belongs_to :server_provider + + validates_presence_of :name, :url, :server_provider_id + + has_many :refs, dependent: :destroy + has_many :permissions, class_name: 'RepositoryPermission', dependent: :delete_all + has_many :users, through: :permissions + has_many :commits, dependent: :destroy + has_many :webhooks, dependent: :destroy + + def branches + refs.branch + end + + def tags + refs.tag + end + + def repo(username = nil, token = nil) + server_provider.bare_repo(self, username, token) + end + + def file_contents(ref, path) + repo.file_contents(ref, path) + end +end diff --git a/app/models/repository_permission.rb b/app/models/repository_permission.rb new file mode 100644 index 00000000..7fc67aaa --- /dev/null +++ b/app/models/repository_permission.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class RepositoryPermission < ApplicationRecord + belongs_to :repository + belongs_to :user + + enum permission: %i[read write admin super] + + def owner? + admin? || super? + end +end diff --git a/app/models/server_provider.rb b/app/models/server_provider.rb new file mode 100644 index 00000000..932fb78b --- /dev/null +++ b/app/models/server_provider.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class ServerProvider < ApplicationRecord + has_many :server_provider_permissions, dependent: :destroy + has_many :users, through: :server_provider_permissions + + has_many :repositories, dependent: :destroy + + validates_presence_of :name, :url, :type + + has_settings(persistent: true) do |s| + s.key :generic + end + + before_save :generate_listener_token + + def bare_repo(*args) + raise NotImplementedError + end + + def remote_repositories + raise NotImplementedError + end + + def commit_info_from_webhook(payload) + raise NotImplementedError + end + + def provider_type + raise NotImplementedError + end + + def default_branch + raise NotImplementedError + end + + private + + def generate_listener_token + return if listener_token.present? + + self.listener_token = SecureRandom.hex(40) + end +end diff --git a/app/models/server_provider_permission.rb b/app/models/server_provider_permission.rb new file mode 100644 index 00000000..a88107df --- /dev/null +++ b/app/models/server_provider_permission.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class ServerProviderPermission < ApplicationRecord + belongs_to :server_provider + belongs_to :user + + has_one :setting, class_name: 'ServerProviderUserSetting', foreign_key: :server_provider_user_id, dependent: :delete + + enum permission: %i[owner member] +end diff --git a/app/models/server_provider_user_setting.rb b/app/models/server_provider_user_setting.rb new file mode 100644 index 00000000..129a3c2b --- /dev/null +++ b/app/models/server_provider_user_setting.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'P4' + +class ServerProviderUserSetting < ApplicationRecord + include EncryptedToken + + belongs_to :permission, class_name: 'ServerProviderPermission', foreign_key: :server_provider_user_id + validates_presence_of :username, :value, :server_provider_user_id + + def token=(token_value) + self.value = encrypted_token(token_value) + end + + def token + decrypted_token(value) + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 00000000..fa91f081 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class User < ApplicationRecord + include Devise::JWT::RevocationStrategies::JTIMatcher + + default_scope { where(active: true) } + + devise :two_factor_authenticatable, + :registerable, + :validatable, + :confirmable, + :jwt_authenticatable, + :two_factor_backupable, + :recoverable, + jwt_revocation_strategy: self, + otp_secret_encryption_key: Settings.otp_secret_encryption_key + + has_many :access_grants, + class_name: 'Doorkeeper::AccessGrant', + foreign_key: :resource_owner_id, + dependent: :delete_all + + has_many :access_tokens, + class_name: 'Doorkeeper::AccessToken', + foreign_key: :resource_owner_id, + dependent: :delete_all + + validate :password_complexity + + has_many :server_provider_permissions + has_many :server_providers, through: :server_provider_permissions + + has_many :repository_permissions + has_many :repositories, through: :repository_permissions + + def server_provider_permission(server_provider_id) + server_provider_permissions.find_by(server_provider_id: server_provider_id) + end + + def set_server_provider_permission(server_provider_id, permission) + perm = server_provider_permissions.find_or_initialize_by(server_provider_id: server_provider_id) + perm.permission = permission + perm.save + end + + def repository_permission(repository_id) + repository_permissions.find_by(repository_id: repository_id) + end + + def mark_as_deleted + update_columns(email: "deleted_email_#{Kernel.rand(1_000_000_000)}@example.com", name: nil, active: false) + end + + private + + def password_complexity + return if password.blank? + + return unless password =~ /\A[A-Za-z]+\z/ + + errors.add(:password, 'should contain a non-alphabet character (number or special character)') + end +end diff --git a/app/models/webhook.rb b/app/models/webhook.rb new file mode 100644 index 00000000..b2e7592c --- /dev/null +++ b/app/models/webhook.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Webhook < ApplicationRecord + belongs_to :repository + + validates_presence_of :repository_id, :name, :url + + scope :active, -> { where(active: true) } + validates :url, url: { schemes: %w[https http] } +end diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb new file mode 100644 index 00000000..615ef8d8 --- /dev/null +++ b/app/serializers/application_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ApplicationSerializer + include JSONAPI::Serializer + + def to_h + data = serializable_hash + + if data[:data].is_a?(Hash) + data[:data][:attributes] + elsif data[:data].is_a?(Array) + data[:data].map { |x| x[:attributes] } + elsif data[:data].nil? + nil + else + data + end + end +end diff --git a/app/serializers/commit_serializer.rb b/app/serializers/commit_serializer.rb new file mode 100644 index 00000000..0a825802 --- /dev/null +++ b/app/serializers/commit_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CommitSerializer < ApplicationSerializer + attributes :id, :message, :sha, :committed_at + + attributes(:author) do |commit| + { + name: commit.user.name || commit.user.email, + email: commit.user.email, + } + end +end diff --git a/app/serializers/full_ref_serializer.rb b/app/serializers/full_ref_serializer.rb new file mode 100644 index 00000000..4bb312e4 --- /dev/null +++ b/app/serializers/full_ref_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class FullRefSerializer < ApplicationSerializer + attributes :id, :name, :type +end diff --git a/app/serializers/ref_serializer.rb b/app/serializers/ref_serializer.rb new file mode 100644 index 00000000..db59e3c5 --- /dev/null +++ b/app/serializers/ref_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RefSerializer < ApplicationSerializer + attributes :id, :name + + attributes(:commit) do |ref| + commit = ref.commits.order('committed_at DESC').first + + { sha: commit.sha } if commit.present? + end +end diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb new file mode 100644 index 00000000..f86234f9 --- /dev/null +++ b/app/serializers/repository_serializer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class RepositorySerializer < ApplicationSerializer + attributes :id, :name, :url, :token, :last_synced_at, :server_provider_id + + attributes(:permission) { |repo, params| params[:current_user].repository_permission(repo.id)&.permission } + attributes(:default_branch) { |repo| repo.server_provider.default_branch } + attributes(:url) { |repo| URI.join(Settings.web_url, "servers/#{repo.server_provider_id}") } + attributes(:owner) do |repo| + { + id: repo.server_provider.id, + } + end + attributes(:slug) { |repo| "#{repo.server_provider.name}/#{repo.name}" } + attributes(:server_type) { |repo| repo.server_provider.provider_type } +end diff --git a/app/serializers/server_provider_serializer.rb b/app/serializers/server_provider_serializer.rb new file mode 100644 index 00000000..66126009 --- /dev/null +++ b/app/serializers/server_provider_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ServerProviderSerializer < ApplicationSerializer + PROVIDER_KLASS = { + P4ServerProvider => 'perforce', + }.freeze + + PERMISSION = { + nil => '', + 'owner' => 'Owner', + 'member' => 'User', + }.freeze + + attributes :id, :name, :url + + attribute(:type) { |server| PROVIDER_KLASS[server.type.constantize] } + attribute(:username) { |server| server.settings(:p4_host).username } + attribute(:permission) { |server, params| PERMISSION[params[:current_user].server_provider_permission(server.id)&.permission] } +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 00000000..d0979580 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class UserSerializer < ApplicationSerializer + attributes :id, :otp_required_for_login + + attribute(:name) { |user| user.name || user.email } + attribute(:login, &:email) + attribute(:emails) { |user| [user.email] } + attribute(:servers) { |user| user.server_providers.map(&:id) } + attribute(:uuid, &:id) +end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb new file mode 100644 index 00000000..ed85a36f --- /dev/null +++ b/app/serializers/webhook_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class WebhookSerializer < ApplicationSerializer + attributes :id, :name, :url, :active, :insecure_ssl, :created_at +end diff --git a/app/services/authenticate_user_with_server_provider.rb b/app/services/authenticate_user_with_server_provider.rb new file mode 100644 index 00000000..77d96789 --- /dev/null +++ b/app/services/authenticate_user_with_server_provider.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class AuthenticateUserWithServerProvider + def initialize(server_provider_permission, server_provider, username, password) + @server_provider_permission = server_provider_permission + @server_provider = server_provider + @username = username + @password = password + end + + def call + case @server_provider + when P4ServerProvider then authenticate_p4 + end + end + + private + + def authenticate_p4 + begin + ValidateP4Credentials.new(@username, @password, @server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed + return false + end + + setting = @server_provider_permission.setting || @server_provider_permission.build_setting + setting.token = @password + setting.username = @username + setting.save + end +end diff --git a/app/services/trigger_webhooks.rb b/app/services/trigger_webhooks.rb new file mode 100644 index 00000000..dba10b53 --- /dev/null +++ b/app/services/trigger_webhooks.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +require 'securerandom' + +class TriggerWebhooks + class WebhookError < StandardError; end + + def initialize(commit) + @commit = commit + @ref = commit.ref + @repository = @ref.repository + @user = commit.user + @server_provider = @repository.server_provider + end + + def call + @repository.webhooks.active.each do |webhook| + process_webhook(webhook) + rescue StandardError => e + Rails.logger.error "An error happened while processing webhook id=#{webhook.id} name=#{webhook.name}: #{e.message}" + Rails.logger.error 'Partial backtrace:' + Rails.logger.error(e.backtrace.first(20).join("\n")) + end + end + + private + + def process_webhook(webhook) + Rails.logger.info "Triggering webhook id=#{webhook.id} name=#{webhook.name}" + uri = URI(webhook.url) + + res = Net::HTTP.post( + uri, + webhook_payload(webhook), + 'Content-Type' => 'application/json', + 'X-Travisproxy-Event-Id' => SecureRandom.uuid, + 'X-Request-Id' => SecureRandom.uuid + ) + + raise WebhookError, "Request failed: code=#{res.code}, body=#{res.body}" unless res.is_a?(Net::HTTPSuccess) + end + + def webhook_payload(webhook) + JSON.dump( + branch_name: 'main', + sender_id: @commit.user_id.to_s, + new_revision: @commit.sha, + sender_login: @user.email, + server_type: 'perforce', + owner_vcs_id: @server_provider.id.to_s, + sender_vcs_id: @commit.user_id.to_s, + repository: { + id: @repository.id.to_s, + name: @repository.name, + full_name: @repository.name, + slug: @repository.url, + is_private: true, + + }, + commits: [ + { + id: @commit.id.to_s, + sha: @commit.sha, + revision: @commit.sha, + message: @commit.message || '', + committed_at: @commit.committed_at, + commiter_name: @user.name || '', + commiter_email: @user.email || '', + }, + ] + ) + end +end diff --git a/app/services/update_repository_credentials.rb b/app/services/update_repository_credentials.rb new file mode 100644 index 00000000..74ef2d34 --- /dev/null +++ b/app/services/update_repository_credentials.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class UpdateRepositoryCredentials + class ValidationFailed < StandardError; end + + def initialize(entity, username, password) + @entity = entity + @username = username + @password = password + end + + def call # rubocop:disable Metrics/CyclomaticComplexity + server_provider = case @entity + when Repository then @entity.server_provider + when ServerProvider then @entity + end + + case server_provider + when P4ServerProvider + if @username.present? || @password.present? + begin + ValidateP4Credentials.new(@username, @password, server_provider.url).call + rescue ValidateP4Credentials::ValidationFailed + raise ValidationFailed + end + end + + @entity.settings(:p4_host).username = @username + @entity.token = @password + @entity.save + end + end +end diff --git a/app/services/validate_p4_credentials.rb b/app/services/validate_p4_credentials.rb new file mode 100644 index 00000000..b9bfc2e0 --- /dev/null +++ b/app/services/validate_p4_credentials.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'P4' + +class ValidateP4Credentials + class ValidationFailed < StandardError + attr_reader :message + + def initialize(message) + @message = message + end + end + + def initialize(username, token, url) + @username = username + @token = token + @url = url + end + + def call + p4 = P4.new + p4.charset = 'utf8' + p4.port = @url + p4.user = @username + p4.password = @token + p4.ticket_file = '/dev/null' + p4.connect + p4.run_trust('-y') + p4.run_protects + + nil + rescue P4Exception => e + raise ValidationFailed, e.message + end +end diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 00000000..dc55f64f --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

Welcome <%= @email %>!

+ +

You can confirm your account email through the link below:

+ +

<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 00000000..4cbe7044 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

Hello <%= @resource.email %>!

+ +

Someone has requested a link to change your password. You can do this through the link below.

+ +

<%= link_to 'Change my password', URI.join(Settings.web_url, "setup_new_password?reset_password_token=#{@token}").to_s %>

+ +

If you didn't request this, please ignore this email.

+

Your password won't change until you access the link above and create a new one.

diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 00000000..ba7ab887 --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+

+ <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> +

+ +
+<% end %> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 00000000..96a94124 --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
+ <% end %> +<% end %> diff --git a/app/views/feedback_mailer/send_feedback.html.erb b/app/views/feedback_mailer/send_feedback.html.erb new file mode 100644 index 00000000..8d74c390 --- /dev/null +++ b/app/views/feedback_mailer/send_feedback.html.erb @@ -0,0 +1,20 @@ +The account <%= @email %> was removed. +

+<% if @feedback[:reason].present? %> +The reason was <%= @feedback[:reason] %> +<% end -%> +

+<% if @feedback[:text].present? %> +The customer provided the following feedback: +

+--- +<%= @feedback[:text] %> +--- + +<% else %> +The customer provided no further feedback. +<% end %> +

+Best regards, +
+Your Travis CI VCS Proxy Bot diff --git a/app/views/feedback_mailer/send_feedback.text.erb b/app/views/feedback_mailer/send_feedback.text.erb new file mode 100644 index 00000000..ea383ca8 --- /dev/null +++ b/app/views/feedback_mailer/send_feedback.text.erb @@ -0,0 +1,18 @@ +The account <%= @email %> was removed. + +<% if @feedback[:reason].present? %> +The reason was <%= @feedback[:reason] %> +<% end -%> + +<% if @feedback[:text].present? %> +The customer provided the following feedback: + +--- +<%= @feedback[:text] %> +--- +<% else %> +The customer provided no further feedback. +<% end %> + +Best regards, +Your Travis CI VCS Proxy Bot diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 00000000..cbd34d2e --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 00000000..cd9bb66d --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> \ No newline at end of file diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..a71368e3 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,114 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# +# This file was generated by Bundler. +# +# The application 'bundle' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "rubygems" + +m = Module.new do + module_function + + def invoked_as_script? + File.expand_path($0) == File.expand_path(__FILE__) + end + + def env_var_version + ENV["BUNDLER_VERSION"] + end + + def cli_arg_version + return unless invoked_as_script? # don't want to hijack other binstubs + return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` + bundler_version = nil + update_index = nil + ARGV.each_with_index do |a, i| + if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN + bundler_version = a + end + next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ + bundler_version = $1 + update_index = i + end + bundler_version + end + + def gemfile + gemfile = ENV["BUNDLE_GEMFILE"] + return gemfile if gemfile && !gemfile.empty? + + File.expand_path("../../Gemfile", __FILE__) + end + + def lockfile + lockfile = + case File.basename(gemfile) + when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) + else "#{gemfile}.lock" + end + File.expand_path(lockfile) + end + + def lockfile_version + return unless File.file?(lockfile) + lockfile_contents = File.read(lockfile) + return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ + Regexp.last_match(1) + end + + def bundler_version + @bundler_version ||= + env_var_version || cli_arg_version || + lockfile_version + end + + def bundler_requirement + return "#{Gem::Requirement.default}.a" unless bundler_version + + bundler_gem_version = Gem::Version.new(bundler_version) + + requirement = bundler_gem_version.approximate_recommendation + + return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0") + + requirement += ".a" if bundler_gem_version.prerelease? + + requirement + end + + def load_bundler! + ENV["BUNDLE_GEMFILE"] ||= gemfile + + activate_bundler + end + + def activate_bundler + gem_error = activation_error_handling do + gem "bundler", bundler_requirement + end + return if gem_error.nil? + require_error = activation_error_handling do + require "bundler/version" + end + return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) + warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" + exit 42 + end + + def activation_error_handling + yield + nil + rescue StandardError, LoadError => e + e + end +end + +m.load_bundler! + +if m.invoked_as_script? + load Gem.bin_path("bundler", "bundle") +end diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..21d3e02d --- /dev/null +++ b/bin/rails @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative "../config/boot" +require "rails/commands" diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..7327f471 --- /dev/null +++ b/bin/rake @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +load File.expand_path("spring", __dir__) +require_relative "../config/boot" +require "rake" +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..90700ac4 --- /dev/null +++ b/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require "fileutils" + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +FileUtils.chdir APP_ROOT do + # This script is a way to set up or update your development environment automatically. + # This script is idempotent, so that you can run it at any time and get an expectable outcome. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies + system! 'bin/yarn' + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:prepare' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 00000000..b4147e84 --- /dev/null +++ b/bin/spring @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby +if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"]) + gem "bundler" + require "bundler" + + # Load Spring without loading other gems in the Gemfile, for speed. + Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring| + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem "spring", spring.version + require "spring/binstub" + rescue Gem::LoadError + # Ignore when Spring is not installed. + end +end diff --git a/bin/webpack b/bin/webpack new file mode 100755 index 00000000..1031168d --- /dev/null +++ b/bin/webpack @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/webpack_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::WebpackRunner.run(ARGV) +end diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server new file mode 100755 index 00000000..dd966273 --- /dev/null +++ b/bin/webpack-dev-server @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development" +ENV["NODE_ENV"] ||= "development" + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "bundler/setup" + +require "webpacker" +require "webpacker/dev_server_runner" + +APP_ROOT = File.expand_path("..", __dir__) +Dir.chdir(APP_ROOT) do + Webpacker::DevServerRunner.run(ARGV) +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 00000000..9fab2c35 --- /dev/null +++ b/bin/yarn @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +APP_ROOT = File.expand_path('..', __dir__) +Dir.chdir(APP_ROOT) do + yarn = ENV["PATH"].split(File::PATH_SEPARATOR). + select { |dir| File.expand_path(dir) != __dir__ }. + product(["yarn", "yarn.cmd", "yarn.ps1"]). + map { |dir, file| File.expand_path(file, dir) }. + find { |file| File.executable?(file) } + + if yarn + exec yarn, *ARGV + else + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 00000000..6dc83218 --- /dev/null +++ b/config.ru @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application +Rails.application.load_server diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 00000000..f1279167 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,31 @@ +require_relative "boot" + +require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_job/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module App + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.1 + + config.api_only = true + + config.active_job.queue_adapter = :sidekiq + + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + config.eager_load_paths << Rails.root.join("lib") + + config.action_mailer.default_url_options = { host: Settings.api_url } + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 00000000..3cda23b4 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require "bundler/setup" # Set up gems listed in the Gemfile. +require "bootsnap/setup" # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..f39dc046 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: test + +production: + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: app_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 00000000..4357d00c --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +aC4UDQbVKxkKLXuKcWPJzNip+EmQKFA8dURE1krFhgrYcLs48gpnBk1ONgzMbZ6KDtlpktk/dCZTiShHi0ZgBapU5MEAP3GA1q/MAWCHbTZyzy5ph5JQYq86SBLaHpFU5mK31XHB5HyHduuPLhX6xmWDl0b16qv1lEXLy2zWgnFo55Wci90VkA8Fgzy5qJs9/nhtpAkFLeqpfA0o1hf4OEJo1KNrLQha0rda2d1bjVim4d6a726lRXUUhKRHpMAOlSmT8S+1XqR/RC3BGFEeS5iY8J2NIz33IY+RZblpcvhD+KC0lMFW3WiLOCYFL17CF/t+eLmVZ4FqVjXVsWVDnTy4UpAmHIHIwJ3BGVkg4TVH6UnpkyOoebYU0NfNcBmemUf73DZyTl1ZM9EQ334vYy0k+ZdrzvQxG2U1--uVh7a0W+6N8ICXG2--5pRUxbjUtTvQeJuxjCmS1Q== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 00000000..89517814 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,19 @@ +default: &default + adapter: postgresql + encoding: unicode + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + host: <%= ENV['DB_HOST'] || 'localhost' %> + user: <%= ENV['DB_USER'] || 'root' %> + password: <%= ENV['DB_PASSWORD'] || 'root' %> + +development: + <<: *default + database: travis_vcs_proxy_development + +test: + <<: *default + database: travis_vcs_proxy_test + +production: + <<: *default + database: <%= ENV['DB_NAME'] || 'travis_vcs_proxy_production' %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 00000000..cac53157 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative "application" + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 00000000..9937cc62 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,75 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded any time + # it changes. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + # Uncomment if you wish to allow Action Cable access from any origin. + # config.action_cable.disable_request_forgery_protection = true + # + config.debug_exception_response_format = :api + + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: 'mailhog', + port: 1025, + } + + config.hosts.clear +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 00000000..e4c43064 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,114 @@ +require "active_support/core_ext/integer/time" + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Include generic and useful information about system operation, but avoid logging too much + # information to avoid inadvertent exposure of personally identifiable information (PII). + config.log_level = :info + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "app_production" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Log disallowed deprecations. + config.active_support.disallowed_deprecation = :log + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require "syslog/logger" + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + + config.action_mailer.delivery_method = :smtp + config.action_mailer.smtp_settings = { + address: ENV['SMTP_HOST'], + port: ENV['SMTP_PORT'], + user_name: ENV['SMTP_USER'], + password: ENV['SMTP_PASSWORD'], + domain: ENV['SMTP_DOMAIN'], + authentication: 'plain', + enable_starttls_auto: true + } +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 00000000..70791305 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,57 @@ +require "active_support/core_ext/integer/time" + +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = false + config.action_view.cache_template_loading = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raise exceptions for disallowed deprecations. + config.active_support.disallowed_deprecation = :raise + + # Tell Active Support which deprecation messages to disallow. + config.active_support.disallowed_deprecation_warnings = [] + + # Raises error for missing translations. + # config.i18n.raise_on_missing_translations = true + + # Annotate rendered view with file names. + # config.action_view.annotate_rendered_view_with_filenames = true +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..89d2efab --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 00000000..33699c30 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code +# by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". +Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] diff --git a/config/initializers/config.rb b/config/initializers/config.rb new file mode 100644 index 00000000..21ff742b --- /dev/null +++ b/config/initializers/config.rb @@ -0,0 +1,58 @@ +Config.setup do |config| + # Name of the constant exposing loaded settings + config.const_name = 'Settings' + + # Ability to remove elements of the array set in earlier loaded settings file. For example value: '--'. + # + # config.knockout_prefix = nil + + # Overwrite an existing value when merging a `nil` value. + # When set to `false`, the existing value is retained after merge. + # + # config.merge_nil_values = true + + # Overwrite arrays found in previously loaded settings file. When set to `false`, arrays will be merged. + # + # config.overwrite_arrays = true + + # Load environment variables from the `ENV` object and override any settings defined in files. + # + config.use_env = true + + # Define ENV variable prefix deciding which variables to load into config. + # + # Reading variables from ENV is case-sensitive. If you define lowercase value below, ensure your ENV variables are + # prefixed in the same way. + # + # When not set it defaults to `config.const_name`. + # + config.env_prefix = 'SETTINGS' + + # What string to use as level separator for settings loaded from ENV variables. Default value of '.' works well + # with Heroku, but you might want to change it for example for '__' to easy override settings from command line, where + # using dots in variable names might not be allowed (eg. Bash). + # + config.env_separator = '__' + + # Ability to process variables names: + # * nil - no change + # * :downcase - convert to lower case + # + # config.env_converter = :downcase + + # Parse numeric values as integers instead of strings. + # + # config.env_parse_values = true + + # Validate presence and type of specific config values. Check https://github.com/dry-rb/dry-validation for details. + # + # config.schema do + # required(:name).filled + # required(:age).maybe(:int?) + # required(:email).filled(format?: EMAIL_REGEX) + # end + + # Evaluate ERB in YAML config files at load time. + # + # config.evaluate_erb_yaml = true +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 00000000..35d0f26f --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,30 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https +# # If you are using webpack-dev-server then specify webpack-dev-server host +# policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000..5a6a32d3 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb new file mode 100644 index 00000000..a4320769 --- /dev/null +++ b/config/initializers/cors.rb @@ -0,0 +1,10 @@ +Rails.application.config.middleware.insert_before 0, Rack::Cors do + allow do + origins Settings.web_url + + resource '*', + headers: :any, + methods: [:get, :post, :put, :patch, :delete, :options, :head] + end +end + diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 00000000..6807160b --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,299 @@ +# frozen_string_literal: true + +# Assuming you have not yet modified this file, each configuration option below +# is set to its default value. Note that some are commented out while others +# are not: uncommented lines are intended to protect your configuration from +# breaking changes in upgrades (i.e., in the event that future versions of +# Devise change the default values for those options). +# +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + config.warden do |manager| + manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable, :two_factor_backupable) + end + + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '5701b8aeebd54f124d95a5b2f2e5b98079f3730558a0327aff4780b6cc585b3416f4d0c65107a74c362a640daa63006d70b33fa829337abeb3d4a5545301f680' + + # ==> Controller configuration + # Configure the parent class to the devise controllers. + # config.parent_controller = 'DeviseController' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = Settings.mail_from + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. + # For API-only applications to support authentication "out-of-the-box", you will likely want to + # enable this with :database unless you are using a custom strategy. + # The supported strategies are: + # :database = Support basic authentication with authentication key + password + config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 12. If + # using other algorithms, it sets how many times you want the password to be hashed. + # The number of stretches used for generating the hashed password are stored + # with the hashed password. This allows you to change the stretches without + # invalidating existing passwords. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 12 + + # Set up a pepper to generate the hashed password. + # config.pepper = '1c1e453962fd6d7a5235fdc21fd6a37eb43908a7c929199412bf6a2b4e25ee3f60e8e6a53e387f96a6d783a336136e714fd4bfc65031a8cac4c72c0ecb2baf27' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. + # You can also set it to nil, which will allow the user to access the website + # without confirming their account. + # Default is 0.days, meaning the user cannot access the website without + # confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + config.confirm_within = 1.hour + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 6..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 1.hour + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # Is disabled because of 2FA + config.sign_in_after_reset_password = false + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' + + # ==> Configuration for :registerable + + # When set to false, does not sign a user in automatically after their password is + # changed. Defaults to true, so a user is signed in automatically after changing a password. + # config.sign_in_after_change_password = true + # + config.jwt do |jwt| + jwt.secret = Settings.jwt.secret + + jwt.dispatch_requests = [ + ['POST', %r{^/v1/users/login$}], + ['GET', %r{^/v1/users/confirmation}], + ['POST', %r{^/v1/user/two_factor_auth/enable$}], + ] + + jwt.revocation_requests = [ + ['DELETE', %r{^/v1/users/logout$}], + ] + end +end diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000..0056d63c --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,490 @@ +# frozen_string_literal: true + +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (requires ORM extensions installed). + # Check the list of supported ORMs here: https://github.com/doorkeeper-gem/doorkeeper#orms + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + # Put your resource owner authentication logic here. + # Example implementation: + current_user || warden.authenticate!(scope: :user) # redirect_to(URI.join(Settings.web_url, 'sign_in').to_s) + end + + # If you didn't skip applications controller from Doorkeeper routes in your application routes.rb + # file then you need to declare this block in order to restrict access to the web interface for + # adding oauth authorized applications. In other case it will return 403 Forbidden response + # every time somebody will try to access the admin web interface. + # + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # + # if current_user + # head :forbidden unless current_user.admin? + # else + # redirect_to sign_in_url + # end + # end + + # You can use your own model classes if you need to extend (or even override) default + # Doorkeeper models such as `Application`, `AccessToken` and `AccessGrant. + # + # Be default Doorkeeper ActiveRecord ORM uses it's own classes: + # + # access_token_class "Doorkeeper::AccessToken" + # access_grant_class "Doorkeeper::AccessGrant" + # application_class "Doorkeeper::Application" + # + # Don't forget to include Doorkeeper ORM mixins into your custom models: + # + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken - for access token + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessGrant - for access grant + # * ::Doorkeeper::Orm::ActiveRecord::Mixins::Application - for application (OAuth2 clients) + # + # For example: + # + # access_token_class "MyAccessToken" + # + # class MyAccessToken < ApplicationRecord + # include ::Doorkeeper::Orm::ActiveRecord::Mixins::AccessToken + # + # self.table_name = "hey_i_wanna_my_name" + # + # def destroy_me! + # destroy + # end + # end + + # Enables polymorphic Resource Owner association for Access Tokens and Access Grants. + # By default this option is disabled. + # + # Make sure you properly setup you database and have all the required columns (run + # `bundle exec rails generate doorkeeper:enable_polymorphic_resource_owner` and execute Rails + # migrations). + # + # If this option enabled, Doorkeeper will store not only Resource Owner primary key + # value, but also it's type (class name). See "Polymorphic Associations" section of + # Rails guides: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations + # + # [NOTE] If you apply this option on already existing project don't forget to manually + # update `resource_owner_type` column in the database and fix migration template as it will + # set NOT NULL constraint for Access Grants table. + # + # use_polymorphic_resource_owner + + # If you are planning to use Doorkeeper in Rails 5 API-only application, then you might + # want to use API mode that will skip all the views management and change the way how + # Doorkeeper responds to a requests. + # + api_only + + # Enforce token request content type to application/x-www-form-urlencoded. + # It is not enabled by default to not break prior versions of the gem. + # + enforce_content_type + + # 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 2.hours + + # Assign custom TTL for access tokens. Will be used instead of access_token_expires_in + # option if defined. In case the block returns `nil` value Doorkeeper fallbacks to + # +access_token_expires_in+ configuration option value. If you really need to issue a + # non-expiring access token (which is not recommended) then you need to return + # Float::INFINITY from this block. + # + # `context` has the following properties available: + # + # * `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # * `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # * `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # * `resource_owner` - authorized resource owner instance (if present) + # + # custom_access_token_expires_in do |context| + # context.client.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator + # + # access_token_generator '::Doorkeeper::JWT' + + # The controller +Doorkeeper::ApplicationController+ inherits from. + # Defaults to +ActionController::Base+ unless +api_only+ is set, which changes the default to + # +ActionController::API+. The return value of this option must be a stringified class name. + # See https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers + # + # base_controller 'ApplicationController' + + # Reuse access token for the same resource owner within an application (disabled by default). + # + # This option protects your application from creating new tokens before old valid one becomes + # expired so your database doesn't bloat. Keep in mind that when this option is `on` Doorkeeper + # doesn't updates existing token expiration time, it will create a new token instead. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # + # You can not enable this option together with +hash_token_secrets+. + # + # reuse_access_token + + # In case you enabled `reuse_access_token` option Doorkeeper will try to find matching + # token using `matching_token_for` Access Token API that searches for valid records + # in batches in order not to pollute the memory with all the database records. By default + # Doorkeeper uses batch size of 10 000 records. You can increase or decrease this value + # depending on your needs and server capabilities. + # + # token_lookup_batch_size 10_000 + + # Set a limit for token_reuse if using reuse_access_token option + # + # This option limits token_reusability to some extent. + # If not set then access_token will be reused unless it expires. + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189 + # + # This option should be a percentage(i.e. (0,100]) + # + # token_reuse_limit 100 + + # Only allow one valid access token obtained via client credentials + # per client. If a new access token is obtained before the old one + # expired, the old one gets revoked (disabled by default) + # + # When enabling this option, make sure that you do not expect multiple processes + # using the same credentials at the same time (e.g. web servers spanning + # multiple machines and/or processes). + # + # revoke_previous_client_credentials_token + + # Hash access and refresh tokens before persisting them. + # This will disable the possibility to use +reuse_access_token+ + # since plain values can no longer be retrieved. + # + # Note: If you are already a user of doorkeeper and have existing tokens + # in your installation, they will be invalid without adding 'fallback: :plain'. + # + # hash_token_secrets + # By default, token secrets will be hashed using the + # +Doorkeeper::Hashing::SHA256+ strategy. + # + # If you wish to use another hashing implementation, you can override + # this strategy as follows: + # + # hash_token_secrets using: '::Doorkeeper::Hashing::MyCustomHashImpl' + # + # Keep in mind that changing the hashing function will invalidate all existing + # secrets, if there are any. + + # Hash application secrets before persisting them. + # + # hash_application_secrets + # + # By default, applications will be hashed + # with the +Doorkeeper::SecretStoring::SHA256+ strategy. + # + # If you wish to use bcrypt for application secret hashing, uncomment + # this line instead: + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt' + + # When the above option is enabled, and a hashed token or secret is not found, + # you can allow to fall back to another strategy. For users upgrading + # doorkeeper and wishing to enable hashing, you will probably want to enable + # the fallback to plain tokens. + # + # This will ensure that old access tokens and secrets + # will remain valid even if the hashing above is enabled. + # + # This can be done by adding 'fallback: plain', e.g. : + # + # hash_application_secrets using: '::Doorkeeper::SecretStoring::BCrypt', fallback: :plain + + # Issue access tokens with refresh token (disabled by default), you may also + # pass a block which accepts `context` to customize when to give a refresh + # token or not. Similar to +custom_access_token_expires_in+, `context` has + # the following properties: + # + # `client` - the OAuth client application (see Doorkeeper::OAuth::Client) + # `grant_type` - the grant type of the request (see Doorkeeper::OAuth) + # `scopes` - the requested scopes (see Doorkeeper::OAuth::Scopes) + # + 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://doorkeeper.gitbook.io/guides/ruby-on-rails/scopes + # + # default_scopes :public + # optional_scopes :write, :update + + # Allows to restrict only certain scopes for grant_type. + # By default, all the scopes will be available for all the grant types. + # + # Keys to this hash should be the name of grant_type and + # values should be the array of scopes for that grant type. + # Note: scopes should be from configured_scopes (i.e. default or optional) + # + # scopes_by_grant_type password: [:write], client_credentials: [:update] + + # Forbids creating/updating applications with arbitrary scopes that are + # not in configuration, i.e. +default_scopes+ or +optional_scopes+. + # (disabled by default) + # + # enforce_configured_scopes + + # 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 https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # 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 https://github.com/doorkeeper-gem/doorkeeper/wiki/Changing-how-clients-are-authenticated + # for more information on customization + # + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # Callable objects such as proc, lambda, block or any object that responds to + # #call can be used in order to allow conditional checks (to allow non-SSL + # redirects to localhost for example). + # + # force_ssl_in_redirect_uri !Rails.env.development? + # + # force_ssl_in_redirect_uri { |uri| uri.host != 'localhost' } + + # Specify what redirect URI's you want to block during Application creation. + # Any redirect URI is whitelisted by default. + # + # You can use this option in order to forbid URI's with 'javascript' scheme + # for example. + # + # forbid_redirect_uri { |uri| uri.scheme.to_s.downcase == 'javascript' } + + # Allows to set blank redirect URIs for Applications in case Doorkeeper configured + # to use URI-less OAuth grant flows like Client Credentials or Resource Owner + # Password Credentials. The option is on by default and checks configured grant + # types, but you **need** to manually drop `NOT NULL` constraint from `redirect_uri` + # column for `oauth_applications` database table. + # + # You can completely disable this feature with: + # + # allow_blank_redirect_uri false + # + # Or you can define your custom check: + # + # allow_blank_redirect_uri do |grant_flows, client| + # client.superapp? + # end + + # Specify how authorization errors should be handled. + # By default, doorkeeper renders json errors when access token + # is invalid, expired, revoked or has invalid scopes. + # + # If you want to render error response yourself (i.e. rescue exceptions), + # set +handle_auth_errors+ to `:raise` and rescue Doorkeeper::Errors::InvalidToken + # or following specific errors: + # + # Doorkeeper::Errors::TokenForbidden, Doorkeeper::Errors::TokenExpired, + # Doorkeeper::Errors::TokenRevoked, Doorkeeper::Errors::TokenUnknown + # + # handle_auth_errors :raise + + # Customize token introspection response. + # Allows to add your own fields to default one that are required by the OAuth spec + # for the introspection response. It could be `sub`, `aud` and so on. + # This configuration option can be a proc, lambda or any Ruby object responds + # to `.call` method and result of it's invocation must be a Hash. + # + # custom_introspection_response do |token, context| + # { + # "sub": "Z5O3upPC88QrAjx00dis", + # "aud": "https://protected.example.net/resource", + # "username": User.find(token.resource_owner_id).username + # } + # end + # + # or + # + # custom_introspection_response CustomIntrospectionResponder + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w[authorization_code client_credentials] + + # Allows to customize OAuth grant flows that +each+ application support. + # You can configure a custom block (or use a class respond to `#call`) that must + # return `true` in case Application instance supports requested OAuth grant flow + # during the authorization request to the server. This configuration +doesn't+ + # set flows per application, it only allows to check if application supports + # specific grant flow. + # + # For example you can add an additional database column to `oauth_applications` table, + # say `t.array :grant_flows, default: []`, and store allowed grant flows that can + # be used with this application there. Then when authorization requested Doorkeeper + # will call this block to check if specific Application (passed with client_id and/or + # client_secret) is allowed to perform the request for the specific grant type + # (authorization, password, client_credentials, etc). + # + # Example of the block: + # + # ->(flow, client) { client.grant_flows.include?(flow) } + # + # In case this option invocation result is `false`, Doorkeeper server returns + # :unauthorized_client error and stops the request. + # + # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call + # @return [Boolean] `true` if allow or `false` if forbid the request + # + # allow_grant_flow_for_client do |grant_flow, client| + # # `grant_flows` is an Array column with grant + # # flows that application supports + # + # client.grant_flows.include?(grant_flow) + # end + + # If you need arbitrary Resource Owner-Client authorization you can enable this option + # and implement the check your need. Config option must respond to #call and return + # true in case resource owner authorized for the specific application or false in other + # cases. + # + # Be default all Resource Owners are authorized to any Client (application). + # + # authorize_resource_owner_for_client do |client, resource_owner| + # resource_owner.admin? || client.owners_whitelist.include?(resource_owner) + # end + + # Hook into the strategies' request & response life-cycle in case your + # application needs advanced customization or logging: + # + # before_successful_strategy_response do |request| + # puts "BEFORE HOOK FIRED! #{request}" + # end + # + # after_successful_strategy_response do |request, response| + # puts "AFTER HOOK FIRED! #{request}, #{response}" + # end + + # Hook into Authorization flow in order to implement Single Sign Out + # or add any other functionality. Inside the block you have an access + # to `controller` (authorizations controller instance) and `context` + # (Doorkeeper::OAuth::Hooks::Context instance) which provides pre auth + # or auth objects with issued token based on hook type (before or after). + # + # before_successful_authorization do |controller, context| + # Rails.logger.info(controller.request.params.inspect) + # + # Rails.logger.info(context.pre_auth.inspect) + # end + # + # after_successful_authorization do |controller, context| + # controller.session[:logout_urls] << + # Doorkeeper::Application + # .find_by(controller.request.params.slice(:redirect_uri)) + # .logout_uri + # + # Rails.logger.info(context.auth.inspect) + # Rails.logger.info(context.issued_token) + # end + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # Configure custom constraints for the Token Introspection request. + # By default this configuration option allows to introspect a token by another + # token of the same application, OR to introspect the token that belongs to + # authorized client (from authenticated client) OR when token doesn't + # belong to any client (public token). Otherwise requester has no access to the + # introspection and it will return response as stated in the RFC. + # + # Block arguments: + # + # @param token [Doorkeeper::AccessToken] + # token to be introspected + # + # @param authorized_client [Doorkeeper::Application] + # authorized client (if request is authorized using Basic auth with + # Client Credentials for example) + # + # @param authorized_token [Doorkeeper::AccessToken] + # Bearer token used to authorize the request + # + # In case the block returns `nil` or `false` introspection responses with 401 status code + # when using authorized token to introspect, or you'll get 200 with { "active": false } body + # when using authorized client to introspect as stated in the + # RFC 7662 section 2.2. Introspection Response. + # + # Using with caution: + # Keep in mind that these three parameters pass to block can be nil as following case: + # `authorized_client` is nil if and only if `authorized_token` is present, and vice versa. + # `token` will be nil if and only if `authorized_token` is present. + # So remember to use `&` or check if it is present before calling method on + # them to make sure you doesn't get NoMethodError exception. + # + # You can define your custom check: + # + # allow_token_introspection do |token, authorized_client, authorized_token| + # if authorized_token + # # customize: require `introspection` scope + # authorized_token.application == token&.application || + # authorized_token.scopes.include?("introspection") + # elsif token.application + # # `protected_resource` is a new database boolean column, for example + # authorized_client == token.application || authorized_client.protected_resource? + # else + # # public token (when token.application is nil, token doesn't belong to any application) + # true + # end + # end + # + # Or you can completely disable any token introspection: + # + # allow_token_introspection false + # + # If you need to block the request at all, then configure your routes.rb or web-server + # like nginx to forbid the request. + + # WWW-Authenticate Realm (default: "Doorkeeper"). + # + # realm "Doorkeeper" +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..a0b27b96 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [ + :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :otp_attempt +] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 00000000..ac033bf9 --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 00000000..dc189968 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb new file mode 100644 index 00000000..00f64d71 --- /dev/null +++ b/config/initializers/permissions_policy.rb @@ -0,0 +1,11 @@ +# Define an application-wide HTTP permissions policy. For further +# information see https://developers.google.com/web/updates/2018/06/feature-policy +# +# Rails.application.config.permissions_policy do |f| +# f.camera :none +# f.gyroscope :none +# f.microphone :none +# f.usb :none +# f.fullscreen :self +# f.payment :self, "https://secure.example.com" +# end diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb new file mode 100644 index 00000000..f7307fe6 --- /dev/null +++ b/config/initializers/sentry.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +if Settings.sentry.dsn.present? + Sentry.init do |config| + config.dsn = Settings.sentry.dsn + config.breadcrumbs_logger = [:active_support_logger, :http_logger] + end +end + diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 00000000..bbfc3961 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 00000000..260e1c4b --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,65 @@ +# Additional translations at https://github.com/heartcombo/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." + updated: "Your account has been updated successfully." + updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000..e3cf04ca --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,148 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + unspecified_scheme: 'must specify a scheme.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + forbidden_uri: 'is forbidden by the server.' + scopes: + not_match_configured: "doesn't match configured on the server." + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' + redirect_uri: 'Use one line per URI' + blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + confidential: 'Confidential?' + actions: 'Actions' + confidentiality: + 'yes': 'Yes' + 'no': 'No' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'UID' + secret: 'Secret' + secret_hashed: 'Secret hashed' + scopes: 'Scopes' + confidential: 'Confidential' + callback_urls: 'Callback urls' + actions: 'Actions' + not_defined: 'Not defined' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + form_post: + title: 'Submit this form' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + pre_authorization: + status: 'Pre-authorization' + + errors: + messages: + # Common error messages + invalid_request: + unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + missing_param: 'Missing required parameter: %{value}.' + request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' + invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." + 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.' + invalid_code_challenge_method: 'The code challenge method must be plain or S256.' + 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 unconfigured.' + admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + unsupported_response_mode: 'The authorization server does not support this response mode.' + + # 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.' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + revoke: + unauthorized: "You are not authorized to revoke this token" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + title: 'Doorkeeper' + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + home: 'Home' + application: + title: 'OAuth authorization required' diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 00000000..cf9b342d --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..d9b3e836 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,43 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `worker_timeout` threshold that Puma will use to wait before +# terminating a worker in development environments. +# +worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..fba373ac --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,87 @@ +Rails.application.routes.draw do + root to: 'home#index' + + post :listener, to: 'v1/webhooks#receive' + + scope :v1 do + use_doorkeeper do + skip_controllers :applications, :token_info + end + end + + scope :v1, module: :v1 do + devise_for :users, + controllers: { + sessions: 'v1/users/sessions', + registrations: 'v1/users/registrations', + confirmations: 'v1/users/confirmations' + }, + path_names: { + sign_in: 'login', + sign_out: 'logout', + register: 'sign_up' + } + + scope 'users/confirmation' do + devise_scope :user do + post :resend, to: 'users/confirmations#resend', as: :resend_confirmation + end + end + + resource :user, only: [:show] do + collection do + get :emails + patch :update_email + patch :update_password + post :request_password_reset + post :reset_password + post :sync + + get :server_providers + get :repositories + end + + resource :two_factor_auth, controller: 'users/two_factor_auth', only: [] do + collection do + get :url + get :codes + post :enable + end + end + end + + resources :server_providers, only: [:create, :show, :update] do + collection do + get :by_url + post :add_by_url + end + + member do + post :authenticate + post :forget + post :sync + + get :repositories + end + end + + resources :repositories, only: [:show] do + resources :branches, controller: 'repositories/branches', only: [:index, :show] + resources :commits, controller: 'repositories/commits', only: [:index, :show] + resources :webhooks, controller: 'repositories/webhooks', only: [:index, :show, :create, :update] + resources :token, controller: 'repositories/token', only: [] do + collection do + get :get + patch :update + delete :destroy + end + end + + member do + get :refs + get 'content/:ref/(*path)', action: :content, format: false + post :sync + end + end + end +end diff --git a/config/settings.yml b/config/settings.yml new file mode 100644 index 00000000..125dc0ca --- /dev/null +++ b/config/settings.yml @@ -0,0 +1,10 @@ +jwt: + secret: fill-me +otp_secret_encryption_key: 12391230-12e90-2if912d120s912is0921iw9012iw9012wi1290wi1290f139 +p4_token_encryption_key: HTmcYwLgONSdedIPBrUcsVQjJjQkFZBE +web_url: https://travis-vcs-proxy.travis-ci.org +api_url: https://travis-vcs-proxy-api.travis-ci.org +feedback_mail: 'cancellations@travis-ci.com' +mail_from: test@example.com +sentry: + dsn: fill-me diff --git a/config/settings/development.yml b/config/settings/development.yml new file mode 100644 index 00000000..b2d23aac --- /dev/null +++ b/config/settings/development.yml @@ -0,0 +1,3 @@ +web_url: http://localhost:13007 +api_url: http://localhost:13006 +feedback_mail: example@example.com diff --git a/config/settings/production.yml b/config/settings/production.yml new file mode 100644 index 00000000..e69de29b diff --git a/config/settings/test.yml b/config/settings/test.yml new file mode 100644 index 00000000..e69de29b diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..db5bf130 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +Spring.watch( + ".ruby-version", + ".rbenv-vars", + "tmp/restart.txt", + "tmp/caching-dev.txt" +) diff --git a/config/webpack/development.js b/config/webpack/development.js new file mode 100644 index 00000000..c5edff94 --- /dev/null +++ b/config/webpack/development.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/environment.js b/config/webpack/environment.js new file mode 100644 index 00000000..d16d9af7 --- /dev/null +++ b/config/webpack/environment.js @@ -0,0 +1,3 @@ +const { environment } = require('@rails/webpacker') + +module.exports = environment diff --git a/config/webpack/production.js b/config/webpack/production.js new file mode 100644 index 00000000..be0f53aa --- /dev/null +++ b/config/webpack/production.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'production' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpack/test.js b/config/webpack/test.js new file mode 100644 index 00000000..c5edff94 --- /dev/null +++ b/config/webpack/test.js @@ -0,0 +1,5 @@ +process.env.NODE_ENV = process.env.NODE_ENV || 'development' + +const environment = require('./environment') + +module.exports = environment.toWebpackConfig() diff --git a/config/webpacker.yml b/config/webpacker.yml new file mode 100644 index 00000000..a6b14656 --- /dev/null +++ b/config/webpacker.yml @@ -0,0 +1,92 @@ +# Note: You must restart bin/webpack-dev-server for changes to take effect + +default: &default + source_path: app/javascript + source_entry_path: packs + public_root_path: public + public_output_path: packs + cache_path: tmp/cache/webpacker + webpack_compile_output: true + + # Additional paths webpack should lookup modules + # ['app/assets', 'engine/foo/app/assets'] + additional_paths: [] + + # Reload manifest.json on all requests so we reload latest compiled packs + cache_manifest: false + + # Extract and emit a css file + extract_css: false + + static_assets_extensions: + - .jpg + - .jpeg + - .png + - .gif + - .tiff + - .ico + - .svg + - .eot + - .otf + - .ttf + - .woff + - .woff2 + + extensions: + - .mjs + - .js + - .sass + - .scss + - .css + - .module.sass + - .module.scss + - .module.css + - .png + - .svg + - .gif + - .jpeg + - .jpg + +development: + <<: *default + compile: true + + # Reference: https://webpack.js.org/configuration/dev-server/ + dev_server: + https: false + host: localhost + port: 3035 + public: localhost:3035 + hmr: false + # Inline should be set to true if using HMR + inline: true + overlay: true + compress: true + disable_host_check: true + use_local_ip: false + quiet: false + pretty: false + headers: + 'Access-Control-Allow-Origin': '*' + watch_options: + ignored: '**/node_modules/**' + + +test: + <<: *default + compile: true + + # Compile test packs to a separate directory + public_output_path: packs-test + +production: + <<: *default + + # Production depends on precompilation of packs prior to booting for performance. + compile: false + + # Extract and emit a css file + extract_css: true + + # Cache manifest.json for performance + cache_manifest: true diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 00000000..650a96c1 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,198 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_08_18_115729) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "commits", force: :cascade do |t| + t.string "sha", null: false + t.integer "user_id", null: false + t.integer "repository_id", null: false + t.integer "ref_id", null: false + t.string "message" + t.datetime "committed_at", null: false + t.datetime "created_at", null: false + t.index ["ref_id", "sha"], name: "index_commits_on_ref_id_and_sha" + t.index ["repository_id"], name: "index_commits_on_repository_id" + t.index ["user_id"], name: "index_commits_on_user_id" + end + + create_table "oauth_access_grants", force: :cascade do |t| + t.bigint "resource_owner_id", null: false + t.bigint "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "scopes", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_grants_on_application_id" + t.index ["resource_owner_id"], name: "index_oauth_access_grants_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true + end + + create_table "oauth_access_tokens", force: :cascade do |t| + t.bigint "resource_owner_id" + t.bigint "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" + t.string "previous_refresh_token", default: "", null: false + t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id" + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true + end + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.boolean "confidential", default: true, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true + end + + create_table "pull_requests", force: :cascade do |t| + t.integer "base_id", null: false + t.integer "head_id", null: false + t.integer "repository_id", null: false + t.integer "user_id", null: false + t.datetime "created_at" + t.integer "number" + t.index ["repository_id"], name: "index_pull_requests_on_repository_id" + t.index ["user_id"], name: "index_pull_requests_on_user_id" + end + + create_table "refs", force: :cascade do |t| + t.string "name", null: false + t.integer "type", null: false + t.integer "repository_id", null: false + t.index ["repository_id", "type", "name"], name: "index_refs_on_repository_id_and_type_and_name", unique: true + end + + create_table "repositories", force: :cascade do |t| + t.string "name", null: false + t.string "url", null: false + t.integer "server_provider_id", null: false + t.datetime "last_synced_at" + t.index ["server_provider_id", "name"], name: "index_repositories_on_server_provider_id_and_name", unique: true + end + + create_table "repository_permissions", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "repository_id", null: false + t.integer "permission", null: false + t.index ["repository_id"], name: "index_repository_permissions_on_repository_id" + t.index ["user_id"], name: "index_repository_permissions_on_user_id" + end + + create_table "server_provider_permissions", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "server_provider_id", null: false + t.integer "permission", null: false + t.index ["server_provider_id"], name: "index_server_provider_permissions_on_server_provider_id" + t.index ["user_id"], name: "index_server_provider_permissions_on_user_id" + end + + create_table "server_provider_user_settings", force: :cascade do |t| + t.string "username", null: false + t.string "value", null: false + t.integer "server_provider_user_id", null: false + t.boolean "is_syncing" + t.index ["server_provider_user_id"], name: "index_server_provider_user_settings_on_server_provider_user_id" + end + + create_table "server_providers", force: :cascade do |t| + t.string "name", null: false + t.string "url", null: false + t.string "type", null: false + t.string "listener_token" + t.index ["listener_token"], name: "index_server_providers_on_listener_token", unique: true + t.index ["type", "url"], name: "index_server_providers_on_type_and_url", unique: true + end + + create_table "settings", force: :cascade do |t| + t.string "var", null: false + t.text "value" + t.string "target_type", null: false + t.bigint "target_id", null: false + t.datetime "created_at", precision: 6 + t.datetime "updated_at", precision: 6 + t.index ["target_type", "target_id", "var"], name: "index_settings_on_target_type_and_target_id_and_var", unique: true + t.index ["target_type", "target_id"], name: "index_settings_on_target" + end + + create_table "users", force: :cascade do |t| + t.string "name" + t.string "email", null: false + t.string "encrypted_password", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.string "encrypted_otp_secret" + t.string "encrypted_otp_secret_iv" + t.string "encrypted_otp_secret_salt" + t.integer "consumed_timestep" + t.boolean "otp_required_for_login" + t.string "otp_backup_codes", array: true + t.string "jti" + t.string "confirmation_token" + t.datetime "confirmation_sent_at" + t.datetime "confirmed_at" + t.string "unconfirmed_email" + t.string "type", default: "", null: false + t.datetime "created_at", null: false + t.boolean "active", default: true + t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["jti"], name: "index_users_on_jti", unique: true + end + + create_table "webhooks", force: :cascade do |t| + t.bigint "repository_id" + t.string "name" + t.string "url" + t.boolean "active" + t.boolean "insecure_ssl" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["repository_id"], name: "index_webhooks_on_repository_id" + end + + add_foreign_key "commits", "refs" + add_foreign_key "commits", "repositories" + add_foreign_key "commits", "users" + add_foreign_key "oauth_access_grants", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id" + add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" + add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id" + add_foreign_key "pull_requests", "commits", column: "base_id" + add_foreign_key "pull_requests", "commits", column: "head_id" + add_foreign_key "pull_requests", "repositories" + add_foreign_key "pull_requests", "users" + add_foreign_key "refs", "repositories" + add_foreign_key "repositories", "server_providers" + add_foreign_key "repository_permissions", "repositories" + add_foreign_key "repository_permissions", "users" + add_foreign_key "server_provider_permissions", "server_providers" + add_foreign_key "server_provider_permissions", "users" + add_foreign_key "server_provider_user_settings", "server_provider_permissions", column: "server_provider_user_id" + add_foreign_key "webhooks", "repositories" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 00000000..f3a0480d --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..28da0827 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +bundle + +exec "$@" diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 00000000..e69de29b diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/lib/travis/vcs_proxy/repositories/p4.rb b/lib/travis/vcs_proxy/repositories/p4.rb new file mode 100644 index 00000000..35480723 --- /dev/null +++ b/lib/travis/vcs_proxy/repositories/p4.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'P4' + +module Travis + module VcsProxy + module Repositories + class P4 + class PermissionNotFound < StandardError; end + + def initialize(repository, url, username, token) + @repository = repository + @url = url + @username = username + @token = token + end + + def repositories + @repositories ||= p4.run_depots.map do |depot| + { + name: depot['name'], + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def branches + @branches ||= p4.run_streams("//#{@repository.name}/...").map do |stream| + { + name: stream['Stream'], + } + end + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def users + @users ||= p4.run_users('-a').map do |user| + next unless user['Email'].include?('@') + + { + email: user['Email'], + name: user['User'], + } + end.compact + rescue P4Exception => e + puts e.message.inspect + + [] + end + + def commits(branch_name) + user_map = users.each_with_object({}) do |user, memo| + memo[user[:name]] = user[:email] + end + p4.run_changes('-l', "//#{@repository.name}/#{branch_name}/...").map do |change| + next unless email = user_map[change['user']] + next unless user = User.find_by(email: email) + + { + sha: change['change'], + user: user, + message: change['desc'], + committed_at: Time.at(change['time'].to_i), + } + end.compact + end + + def permissions + @permissions ||= users.each_with_object({}) do |user, memo| + memo[user[:email]] = p4.run_protects('-u', user[:name], '-M', "//#{@repository.name}/...").first['permMax'] + rescue P4Exception => e + puts e.message.inspect + end + end + + def file_contents(ref, path) + p4.run_print("//#{@repository.name}/#{path}") + rescue P4Exception => e + puts e.message.inspect + + nil + end + + def commit_info(change_root, username) + matches = change_root.match(%r{\A//([^/]+)/([^/]+)}) + return if matches.nil? + + { + repository_name: matches[1], + ref: matches[2], + email: p4.run_user('-o', username).first['Email'].encode('utf-8'), + } + rescue P4Exception => e + puts e.message.inspect + + nil + end + + private + + def p4 + return @p4 if defined?(@p4) + + @p4 = ::P4.new + @p4.charset = 'utf8' + @p4.port = @url + @p4.user = @username + @p4.password = @token + @p4.ticket_file = '/dev/null' + @p4.connect + @p4.run_trust('-y') + @p4.run_protects + + @p4 + rescue P4Exception => e + puts e.message.inspect + raise + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/repository.rb b/lib/travis/vcs_proxy/sync/repository.rb new file mode 100644 index 00000000..010aa8d7 --- /dev/null +++ b/lib/travis/vcs_proxy/sync/repository.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class Repository + def initialize(repository, user) + @repository = repository + @user = user + end + + def sync # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength + username = @repository.settings(:p4_host).username + token = @repository.token + if username.blank? || token.blank? + if server_provider_user_setting = @user.server_provider_permission(@repository.server_provider_id)&.setting + username = server_provider_user_setting.username + token = server_provider_user_setting.token + end + end + + if username.blank? || token.blank? + username = @repository.server_provider.settings(:p4_host).username + token = @repository.server_provider.token + end + + return if username.blank? || token.blank? + + repo = @repository.repo(username, token) + + repo.branches.each do |branch| + branch_name = branch[:name].sub(%r{\A//#{Regexp.escape(@repository.name)}/}, '') + branch = @repository.branches.find_or_create_by!(name: branch_name) + + repo.commits(branch_name).each do |commit| + next if branch.commits.where(sha: commit[:sha]).exists? + + branch.commits.create!(commit.merge(repository_id: @repository.id)) + end + end + + perms = repo.permissions + if perms.present? + repo_emails = perms.keys + users = ::User.where(email: repo_emails).group_by(&:email) + db_emails = @repository.users.pluck(:email) + + # Remove users that don't have access anymore + (db_emails - repo_emails).each do |email| + users[email].first.repository_permission(@repository.id).delete + end + + perms.each do |email, permission| + next unless users.key?(email) + + user = users[email].first + perm = user.repository_permission(@repository.id) + if permission == 'none' + perm&.delete + break + end + + perm ||= user.repository_permissions.build(repository_id: @repository.id) + perm.permission = permission + perm.save! + end + end + + @repository.last_synced_at = Time.now + @repository.save! + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/server_provider.rb b/lib/travis/vcs_proxy/sync/server_provider.rb new file mode 100644 index 00000000..058e51d0 --- /dev/null +++ b/lib/travis/vcs_proxy/sync/server_provider.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class ServerProvider + def initialize(server_provider, user) + @server_provider = server_provider + @user = user + end + + def sync + ActiveRecord::Base.transaction do + sync_repositories(@server_provider.remote_repositories) + + @server_provider.users.each do |user| + next unless permission_setting = user.server_provider_permission(@server_provider.id).setting + + sync_repositories(@server_provider.remote_repositories(permission_setting.username, permission_setting.token)) + end + end + end + + private + + def sync_repositories(repositories) + repositories.each do |repository| + repo = @server_provider.repositories.find_or_initialize_by(name: repository[:name]) + unless repo.persisted? + repo.url = 'STUB' + repo.save! + end + + Travis::VcsProxy::Sync::Repository.new(repo, @user).sync + end + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/sync/user.rb b/lib/travis/vcs_proxy/sync/user.rb new file mode 100644 index 00000000..bbbe33b1 --- /dev/null +++ b/lib/travis/vcs_proxy/sync/user.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + module Sync + class User + def initialize(user) + @user = user + end + + def sync + @user.server_providers.each do |server_provider| + Travis::VcsProxy::Sync::ServerProvider.new(server_provider, @user).sync + end + end + end + end + end +end diff --git a/lib/travis/vcs_proxy/syncer.rb b/lib/travis/vcs_proxy/syncer.rb new file mode 100644 index 00000000..2ce5ecb1 --- /dev/null +++ b/lib/travis/vcs_proxy/syncer.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Travis + module VcsProxy + class Syncer + def initialize(user) + @user = user + end + + def sync_server_provider(server_provider) + Sync::ServerProvider.new(server_provider, @user).sync + end + + def sync_user + Sync::User.new(@user).sync + end + + def sync_repository(repository) + Sync::Repository.new(repository, @user).sync + end + end + end +end diff --git a/log/.keep b/log/.keep new file mode 100644 index 00000000..e69de29b diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..2be3af26 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/422.html b/public/422.html new file mode 100644 index 00000000..c08eac0d --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/500.html b/public/500.html new file mode 100644 index 00000000..78a030af --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 00000000..e69de29b diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 00000000..e69de29b diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..e69de29b diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..c19f78ab --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/spec/controllers/v1/repositories/branches_controller_spec.rb b/spec/controllers/v1/repositories/branches_controller_spec.rb new file mode 100644 index 00000000..c60fc97c --- /dev/null +++ b/spec/controllers/v1/repositories/branches_controller_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Repositories::BranchesController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + + before do + sign_in(user) + end + + describe 'GET index' do + it 'returns branches for specified repository' do + get :index, params: { repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump([{ id: branch_ref.id, name: branch_ref.name, commit: nil }])) + end + end + + describe 'GET show' do + it 'returns specified branch from specified repository' do + get :show, params: { repository_id: repository.id, id: branch_ref.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(id: branch_ref.id, name: branch_ref.name, commit: nil)) + end + end +end diff --git a/spec/controllers/v1/repositories/commits_controller_spec.rb b/spec/controllers/v1/repositories/commits_controller_spec.rb new file mode 100644 index 00000000..f44c9719 --- /dev/null +++ b/spec/controllers/v1/repositories/commits_controller_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Repositories::CommitsController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } + + before do + sign_in(user) + end + + describe 'GET index' do + it 'returns commits for specified ref and repository' do + get :index, params: { repository_id: repository.id, branch: branch_ref.name } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + [ + { + id: commit.id, + message: commit.message, + sha: commit.sha, + committed_at: commit.committed_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + author: { + name: user.name, + email: user.email, + }, + }, + ] + )) + end + end + + describe 'GET show' do + it 'returns specified branch from specified repository' do + get :show, params: { repository_id: repository.id, branch: branch_ref.name, id: commit.sha } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: commit.id, + message: commit.message, + sha: commit.sha, + committed_at: commit.committed_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + author: { + name: user.name, + email: user.email, + } + )) + end + end +end diff --git a/spec/controllers/v1/repositories/token_controller_spec.rb b/spec/controllers/v1/repositories/token_controller_spec.rb new file mode 100644 index 00000000..106d5bb6 --- /dev/null +++ b/spec/controllers/v1/repositories/token_controller_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Repositories::TokenController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + let!(:commit) { FactoryBot.create(:commit, ref: branch_ref, repository: repository, user: user) } + + before do + sign_in(user) + end + + describe 'PATCH update' do + let(:token) { 'token' } + let(:username) { 'username' } + + context 'when username and token are present and user has permission' do + it 'updates the token' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, username, token).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_successful + end + + it 'returns error on unsuccessful update' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, username, token).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response.status).to eq(422) + end + end + + context 'when username or token are blank' do + let(:token) { '' } + let(:username) { '' } + + it 'returns bad_request' do + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_bad_request + end + end + + context 'when user does not have permission' do + before do + repository_permission.permission = :read + repository_permission.save + end + + it 'returns bad_request' do + patch :update, params: { repository_id: repository.id, username: username, token: token } + + expect(response).to be_forbidden + end + end + end + + describe 'DELETE destroy' do + context 'when username and token are present and user has permission' do + it 'destroys the token' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, nil, nil).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + + delete :destroy, params: { repository_id: repository.id } + + expect(response).to be_successful + end + + it 'returns error on unsuccessful update' do + expect(UpdateRepositoryCredentials).to receive(:new).with(repository, nil, nil).and_call_original + expect_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + + delete :destroy, params: { repository_id: repository.id } + + expect(response.status).to eq(422) + end + end + + context 'when user does not have permission' do + before do + repository_permission.permission = :read + repository_permission.save + end + + it 'returns bad_request' do + delete :destroy, params: { repository_id: repository.id } + + expect(response).to be_forbidden + end + end + end +end diff --git a/spec/controllers/v1/repositories/webhooks_controller_spec.rb b/spec/controllers/v1/repositories/webhooks_controller_spec.rb new file mode 100644 index 00000000..1e198ace --- /dev/null +++ b/spec/controllers/v1/repositories/webhooks_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Repositories::WebhooksController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:webhook) { FactoryBot.create(:webhook, repository: repository) } + + before do + sign_in(user) + end + + describe 'GET index' do + it 'returns webhooks for specified repository' do + get :index, params: { repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + [ + { + id: webhook.id, + name: webhook.name, + url: webhook.url, + active: webhook.active, + insecure_ssl: webhook.insecure_ssl, + created_at: webhook.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + }, + ] + )) + end + end + + describe 'GET show' do + it 'returns webhook for specified repository and id' do + get :show, params: { id: webhook.id, repository_id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: webhook.id, + name: webhook.name, + url: webhook.url, + active: webhook.active, + insecure_ssl: webhook.insecure_ssl, + created_at: webhook.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ') + )) + end + end + + describe 'POST create' do + let(:webhook_params) do + { + name: 'TestHook', + url: 'https://test.url/hook', + active: true, + insecure_ssl: false, + } + end + + it 'creates the webhook and returns its representation' do + post :create, params: { repository_id: repository.id, webhook: webhook_params } + + expect(response).to be_successful + data = JSON.parse(response.body) + expect(data['name']).to eq(webhook_params[:name]) + expect(data['url']).to eq(webhook_params[:url]) + expect(data['active']).to eq(webhook_params[:active]) + expect(data['insecure_ssl']).to eq(webhook_params[:insecure_ssl]) + end + end + + describe 'PATCH update' do + let(:webhook_params) do + { + name: 'TestHookUpdate', + } + end + + it 'updates the webhook and returns its representation' do + patch :update, params: { id: webhook.id, repository_id: repository.id, webhook: webhook_params } + + expect(response).to be_successful + data = JSON.parse(response.body) + expect(data['name']).to eq(webhook_params[:name]) + end + end +end diff --git a/spec/controllers/v1/repositories_controller_spec.rb b/spec/controllers/v1/repositories_controller_spec.rb new file mode 100644 index 00000000..404e57b6 --- /dev/null +++ b/spec/controllers/v1/repositories_controller_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::RepositoriesController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:server_provider) { FactoryBot.create(:p4_server_provider) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: repository, type: :branch) } + + before do + sign_in(user) + end + + describe 'GET show' do + it 'returns specified repository' do + get :show, params: { id: repository.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: repository.id, + name: repository.name, + url: URI.join(Settings.web_url, "servers/#{repository.server_provider_id}"), + token: repository.token, + last_synced_at: repository.last_synced_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + server_provider_id: repository.server_provider_id, + permission: repository_permission.permission, + default_branch: server_provider.default_branch, + owner: { + id: server_provider.id, + }, + slug: "#{server_provider.name}/#{repository.name}", + server_type: server_provider.provider_type + )) + end + end + + describe 'GET refs' do + it 'returns refs from specified repository' do + get :refs, params: { id: repository.id } + + expect(response.body).to eq(JSON.dump([ + { + id: branch_ref.id, + name: branch_ref.name, + type: branch_ref.type, + }, + ])) + end + end + + describe 'GET content' do + context 'when no path is specified' do + it 'returns an error' do + get :content, params: { id: repository.id, ref: branch_ref.id } + + expect(response).to be_bad_request + end + end + + context 'when path is specified' do + let(:path) { 'path' } + let(:contents) { ['', 'contents'] } + + it 'returns the contents' do + expect_any_instance_of(Repository).to receive(:file_contents).and_return(contents) + + get :content, params: { id: repository.id, ref: branch_ref.id, path: path } + + expect(response).to be_successful + expect(response.body).to eq(contents[1]) + end + end + + context 'when path is specified but contents are blank' do + let(:path) { 'path' } + let(:contents) { '' } + + it 'returns the contents' do + expect_any_instance_of(Repository).to receive(:file_contents).and_return(contents) + + get :content, params: { id: repository.id, ref: branch_ref.id, path: path } + + expect(response.status).to eq(422) + end + end + end +end diff --git a/spec/controllers/v1/server_providers_controller_spec.rb b/spec/controllers/v1/server_providers_controller_spec.rb new file mode 100644 index 00000000..7d07bef3 --- /dev/null +++ b/spec/controllers/v1/server_providers_controller_spec.rb @@ -0,0 +1,280 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::ServerProvidersController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + describe 'POST create' do + let(:params) do + { + server_provider: { + type: 'perforce', + url: 'test.e.corp', + name: 'EcorpServer', + username: 'user', + token: 'token', + }, + } + end + + context 'when no type provided' do + before { params[:server_provider][:type] = '' } + + it 'returns an error' do + post :create, params: params + + expect(response).to be_bad_request + end + end + + context 'when server with provided URL already exists' do + let!(:server_provider) { FactoryBot.create(:server_provider, url: params[:server_provider][:url]) } + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['A server with this URL already exists.'])) + end + end + + context 'when all parameters present but validation fails' do + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + end + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot save credentials'])) + end + end + + context 'when all parameters present but setting the permission fails' do + before do + allow_any_instance_of(User).to receive(:set_server_provider_permission).and_return(false) + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + it 'returns an error' do + post :create, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot set permission for user'])) + end + end + + context 'when all parameters present' do + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + it 'creates the server provider' do + expect { post :create, params: params }.to change(ServerProvider, :count).by(1) + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: ServerProvider.last.id, + name: params[:server_provider][:name], + url: params[:server_provider][:url], + type: params[:server_provider][:type], + username: '', + permission: 'Owner' + )) + expect(ServerProvider.last.name).to eq(params[:server_provider][:name]) + end + end + end + + describe 'GET show' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns the server provider representation' do + get :show, params: { id: server_provider.id } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: server_provider.id, + name: server_provider.name, + url: server_provider.url, + type: 'perforce', + username: '', + permission: 'Owner' + )) + end + end + + describe 'PATCH update' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:params) do + { + id: server_provider.id, + server_provider: { + name: 'TestNameUpdate', + username: 'user', + token: 'token', + }, + } + end + + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(true) + end + + context 'when user has permission' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'updates the server provider' do + patch :update, params: params + + expect(response).to be_successful + expect(ServerProvider.last.name).to eq(params[:server_provider][:name]) + end + end + + context 'when user has no permission' do + it 'returns forbidden' do + patch :update, params: params + + expect(response).to be_forbidden + end + end + + context 'when credentials are not validated' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + before do + allow_any_instance_of(UpdateRepositoryCredentials).to receive(:call).and_return(false) + end + + it 'returns an error' do + patch :update, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot save credentials'])) + end + end + end + + describe 'POST authenticate' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let(:params) do + { + id: server_provider.id, + username: 'user', + token: 'token', + } + end + + context 'when no username or token are provided' do + before { params[:username] = '' } + + it 'returns an error' do + post :authenticate, params: params + + expect(response).to be_bad_request + end + end + + context 'when permission exists' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + before do + allow_any_instance_of(ValidateP4Credentials).to receive(:call).and_return(true) + end + + it 'updates credentials' do + post :authenticate, params: params + + expect(response).to be_successful + expect(server_provider_permission.setting.username).to eq(params[:username]) + expect(server_provider_permission.setting.token).to eq(params[:token]) + end + end + + context 'when permission does not exist' do + before do + allow_any_instance_of(ValidateP4Credentials).to receive(:call).and_return(true) + end + + it 'creates member permission and updates credentials' do + post :authenticate, params: params + + expect(response).to be_successful + server_provider_permission = ServerProviderPermission.last + expect(server_provider_permission.permission).to eq('member') + expect(server_provider_permission.setting.username).to eq(params[:username]) + expect(server_provider_permission.setting.token).to eq(params[:token]) + end + end + + context 'when validation fails' do + it 'creates member permission and updates credentials' do + post :authenticate, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Cannot authenticate'])) + end + end + end + + describe 'POST forget' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'removes server provider permission' do + expect { post :forget, params: { id: server_provider.id } }.to change(ServerProviderPermission, :count).by(-1) + end + end + + describe 'POST sync' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'schedules sync for server provider' do + expect(SyncJob).to receive(:perform_later).with(SyncJob::SyncType::SERVER_PROVIDER, server_provider.id, user.id) + + post :sync, params: { id: server_provider.id } + + expect(response).to be_successful + end + end + + describe 'GET by_url' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns server provider representation' do + get :by_url, params: { url: server_provider.url } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: server_provider.id, + name: server_provider.name, + url: server_provider.url, + type: 'perforce', + username: '', + permission: 'Owner' + )) + end + end + + describe 'POST add_by_url' do + let(:server_provider) { FactoryBot.create(:server_provider) } + + it 'adds member permission for user' do + post :add_by_url, params: { url: server_provider.url } + + expect(response).to be_successful + server_provider_permission = ServerProviderPermission.last + expect(server_provider_permission.permission).to eq('member') + end + end +end diff --git a/spec/controllers/v1/users/confirmations_controller_spec.rb b/spec/controllers/v1/users/confirmations_controller_spec.rb new file mode 100644 index 00000000..729f86bb --- /dev/null +++ b/spec/controllers/v1/users/confirmations_controller_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Users::ConfirmationsController, type: :controller do + let(:user) { FactoryBot.create(:user) } + + before { @request.env['devise.mapping'] = Devise.mappings[:user] } + + describe 'GET show' do + let(:confirmation_token) { 'TOKTOKTOKTOKTOK' } + + before do + allow(User).to receive(:confirm_by_token).with(confirmation_token).and_return(user) + end + + context 'when confirmation token is valid' do + it 'confirms the user and redirects to confirmed url' do + get :show, params: { confirmation_token: confirmation_token } + + expect(response).to redirect_to(URI.join(Settings.web_url, 'confirmed').to_s) + end + end + + context 'when confirmation token is invalid' do + before do + user.errors.add('token invalid') + end + + it 'redirects to unconfirmed page' do + get :show, params: { confirmation_token: confirmation_token } + + redirect_uri = URI.join(Settings.web_url, 'unconfirmed') + redirect_uri.query = 'error=expired' + expect(response).to redirect_to(redirect_uri.to_s) + end + end + end + + describe 'POST resend' do + context 'when email is present and user is not confirmed' do + before do + allow_any_instance_of(User).to receive(:confirmed?).and_return(false) + end + + it 'resends the confirmation email' do + expect_any_instance_of(User).to receive(:resend_confirmation_instructions) + + post :resend, params: { email: user.email } + + expect(response).to be_successful + end + end + + context 'when email is present and user is confirmed' do + before do + allow_any_instance_of(User).to receive(:confirmed?).and_return(true) + end + + it 'does not resend the confirmation email' do + expect_any_instance_of(User).not_to receive(:resend_confirmation_instructions) + + post :resend, params: { email: user.email } + + expect(response).to be_successful + end + end + + context 'when email is not present' do + it 'responds with bad request' do + post :resend + + expect(response).to be_bad_request + end + end + end +end diff --git a/spec/controllers/v1/users/registrations_controller_spec.rb b/spec/controllers/v1/users/registrations_controller_spec.rb new file mode 100644 index 00000000..a8b3bf84 --- /dev/null +++ b/spec/controllers/v1/users/registrations_controller_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Users::RegistrationsController, type: :controller do + before { @request.env['devise.mapping'] = Devise.mappings[:user] } + + describe 'POST create' do + let(:sign_up_params) do + { + email: 'new@user.com', + password: 'Stronk$PASS123', + password_confirmation: 'Stronk$PASS123', + } + end + + context 'when user creation is successful' do + it 'creates user' do + expect do + post :create, params: { user: sign_up_params } + end.to change(User, :count).by(1) + + expect(response).to be_successful + end + end + + context 'when user creation is unsuccessful' do + before do + sign_up_params[:password_confirmation] = 'e' + end + + it 'does not create user and returns unprocessible entity' do + expect do + post :create, params: { user: sign_up_params } + end.not_to change(User, :count) + + expect(response.status).to eq(422) + end + end + end + + describe 'DELETE destroy' do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + context 'when request has feedback' do + let(:params) do + { + password: 'TestPass#123', + feedback: { + reason: 'Reason', + text: 'Text', + }, + } + end + let(:feedback_mailer) { double } + let(:mail_promise) { double } + + it 'removes user, sends feedback' do + expect(FeedbackMailer).to receive(:with).and_return(feedback_mailer) + expect(feedback_mailer).to receive(:send_feedback).and_return(mail_promise) + expect(mail_promise).to receive(:deliver_now) + + delete :destroy, params: params + + expect(response).to be_successful + expect(user.reload.active).to be_falsey + end + end + + context 'when request does not have feedback' do + let(:params) do + { + password: 'TestPass#123', + } + end + + it 'removes user, sends feedback' do + expect(FeedbackMailer).not_to receive(:with) + + delete :destroy, params: params + + expect(response).to be_successful + expect(user.reload.active).to be_falsey + end + end + + context 'when password is incorrect' do + let(:params) do + { + password: 'Test123', + } + end + + it 'removes user, sends feedback' do + expect(FeedbackMailer).not_to receive(:with) + + delete :destroy, params: params + + expect(response.status).to eq(422) + expect(user.reload.active).to be_truthy + end + end + end +end diff --git a/spec/controllers/v1/users/sessions_controller_spec.rb b/spec/controllers/v1/users/sessions_controller_spec.rb new file mode 100644 index 00000000..abcf8670 --- /dev/null +++ b/spec/controllers/v1/users/sessions_controller_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Users::SessionsController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_secret: User.generate_otp_secret) } + let(:jwt_token) { 'SecretToken' } + + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + allow(controller).to receive(:current_user_jwt_token).and_return(jwt_token) + end + + describe 'POST create' do + context 'when email and password are missing' do + let(:params) do + { + user: { + email: '', + password: '', + }, + } + end + + it 'returns bad request' do + post :create, params: params, format: :json + + expect(response).to be_bad_request + end + end + + context 'when user does not exist' do + let(:params) do + { + user: { + email: 'noemail@e.corp', + password: 'e', + }, + } + end + + it 'returns unauthorized' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + end + end + + context 'when password is incorrect' do + let(:params) do + { + user: { + email: user.email, + password: 'e', + }, + } + end + + it 'returns an error' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + expect(response.body).to eq(JSON.dump(error: 'Invalid Email or password.')) + end + end + + context 'when email and password are correct' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + }, + } + end + + it 'returns token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: false)) + end + end + + context 'when otp is enabled and otp attempt is blank' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns empty token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: '', otp_enabled: true)) + end + end + + context 'when otp is enabled and otp attempt is present and correct' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + otp_attempt: user.current_otp, + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns token and otp enabled' do + post :create, params: params, format: :json + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: true)) + end + end + + context 'when otp is enabled and otp attempt is present and incorrect' do + let(:params) do + { + user: { + email: user.email, + password: user.password, + otp_attempt: 'random', + }, + } + end + + before do + user.otp_required_for_login = true + user.save + end + + it 'returns unauthorized' do + post :create, params: params, format: :json + + expect(response).to be_unauthorized + end + end + end +end diff --git a/spec/controllers/v1/users/two_factor_auth_controller_spec.rb b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb new file mode 100644 index 00000000..d3800d73 --- /dev/null +++ b/spec/controllers/v1/users/two_factor_auth_controller_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::Users::TwoFactorAuthController, type: :controller do + let(:user) { FactoryBot.create(:user) } + + before { sign_in(user) } + + describe 'GET url' do + let(:otp_secret) { 'secret' } + + it 'generates otp secret and returns the url' do + allow(User).to receive(:generate_otp_secret).and_return(otp_secret) + + get :url + + expect(JSON.parse(response.body)).to eq('url' => "otpauth://totp/Travis%20CI%20VCS%20Proxy:bob%40uncle.com?secret=#{otp_secret}&issuer=Travis%20CI%20VCS%20Proxy") + expect(user.reload.otp_secret).to eq(otp_secret) + end + end + + describe 'POST enable' do + before do + user.otp_secret = User.generate_otp_secret + user.save + end + + context 'when provided otp_attempt is incorrect' do + let(:otp_attempt) { '21345' } + + it 'returns an error' do + post :enable, params: { otp_attempt: otp_attempt } + + expect(response.status).to eq(422) + end + end + + context 'when provided otp_attempt is correct' do + let(:otp_attempt) { user.current_otp } + let(:jwt_token) { 'SecretToken' } + + before { allow(controller).to receive(:current_user_jwt_token).and_return(jwt_token) } + + it 'revokes old token and returns new token' do + expect(User).to receive(:revoke_jwt) + + post :enable, params: { otp_attempt: otp_attempt } + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(token: jwt_token, otp_enabled: true)) + end + end + end + + describe 'GET codes' do + let(:codes) { 'codes' } + + context 'when generation is successful' do + before do + allow_any_instance_of(User).to receive(:generate_otp_backup_codes!).and_return(codes) + end + + it 'returns the recoery codes' do + get :codes + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(codes: codes)) + end + end + + context 'when generation is not successful' do + before do + allow_any_instance_of(User).to receive(:save).and_return(false) + end + + it 'returns unprocessible entity' do + get :codes + + expect(response.status).to eq(422) + end + end + end +end diff --git a/spec/controllers/v1/users_controller_spec.rb b/spec/controllers/v1/users_controller_spec.rb new file mode 100644 index 00000000..5d409e42 --- /dev/null +++ b/spec/controllers/v1/users_controller_spec.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::UsersController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + + before do + sign_in(user) + end + + describe 'GET show' do + it 'returns the user representation' do + get :show + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump( + id: user.id, + otp_required_for_login: user.otp_required_for_login, + name: user.name, + login: user.email, + emails: [user.email], + servers: [], + uuid: user.id + )) + end + end + + describe 'PATCH update_email' do + let(:params) do + { + email: 'email@ecorp.com', + } + end + + context 'when email is provided' do + it 'updates the email' do + patch :update_email, params: params + + expect(response).to be_successful + expect(user.reload.unconfirmed_email).to eq(params[:email]) + end + end + + context 'when email is not provided' do + before { params[:email] = '' } + + it 'does not update the email' do + patch :update_email, params: params + + expect(response).to be_bad_request + expect(user.reload.unconfirmed_email).to eq(nil) + end + end + end + + describe 'PATCH update_password' do + let(:new_password) { 'Stronk$Pass123' } + let(:params) do + { + current_password: user.password, + password: new_password, + password_confirmation: new_password, + } + end + + context 'when valid password is provided' do + it 'changes the password' do + patch :update_password, params: params + + expect(response).to be_successful + end + end + + context 'when one of the parameters is not provided' do + before { params[:password] = '' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response).to be_bad_request + end + end + + context 'when new password does not match confirmation' do + before { params[:password] = 'e' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Password does not match confirmation'])) + end + end + + context 'when new password does not meet requirements' do + before do + params[:password] = 'e' + params[:password_confirmation] = 'e' + end + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: { password: ['is too short (minimum is 6 characters)', 'should contain a non-alphabet character (number or special character)'] })) + end + end + + context 'when current password does not match' do + before { params[:current_password] = 'e' } + + it 'returns an error' do + patch :update_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: ['Invalid current password'])) + end + end + end + + describe 'POST request_password_reset' do + let(:params) do + { + email: user.email, + } + end + + context 'when email is blank' do + before { params[:email] = '' } + + it 'returns no error' do + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + + context 'when email does not belong to any user' do + before { params[:email] = 'no@email.exists' } + + it 'returns no error' do + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + + context 'when email belongs to the user' do + it 'returns no error and requests password reset' do + expect_any_instance_of(User).to receive(:send_reset_password_instructions) + + post :request_password_reset, params: params + + expect(response).to be_successful + end + end + end + + describe 'POST reset_password' do + let(:new_password) { 'Stronk$Pass123' } + let(:params) do + { + reset_password_token: 'token', + password: new_password, + password_confirmation: new_password, + } + end + + context 'when all parameters are present and correct' do + before do + allow(User).to receive(:reset_password_by_token).and_return(user) + end + + it 'resets the password' do + post :reset_password, params: params + + expect(response).to be_successful + end + end + + context 'when reset token is not correct' do + before do + allow(User).to receive(:reset_password_by_token).and_return(user) + user.errors.add(:password, 'Failed') + end + + it 'resets the password' do + post :reset_password, params: params + + expect(response.status).to eq(422) + expect(response.body).to eq(JSON.dump(errors: { password: ['Failed'] })) + end + end + end + + describe 'GET emails' do + it 'returns users emails' do + get :emails + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump(emails: [user.email])) + end + end + + describe 'GET server_providers' do + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + + it 'returns users servers' do + get :server_providers + + expect(response).to be_successful + expect(JSON.parse(response.body)['server_providers']).to eq([ + 'id' => server_provider.id, + 'name' => server_provider.name, + 'url' => server_provider.url, + 'type' => 'perforce', + 'username' => '', + 'permission' => 'Owner', + ]) + end + end + + describe 'GET repositories' do + let(:server_provider) { FactoryBot.create(:p4_server_provider) } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + + it 'returns users repositories' do + get :repositories + + expect(response).to be_successful + expect(response.body).to eq(JSON.dump([{ + id: repository.id, + name: repository.name, + url: URI.join(Settings.web_url, "servers/#{repository.server_provider_id}"), + token: repository.token, + last_synced_at: repository.last_synced_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), + server_provider_id: repository.server_provider_id, + permission: repository_permission.permission, + default_branch: server_provider.default_branch, + owner: { + id: server_provider.id, + }, + slug: "#{server_provider.name}/#{repository.name}", + server_type: server_provider.provider_type, + }])) + end + end + + describe 'POST sync' do + it 'schedules sync for server provider' do + expect(SyncJob).to receive(:perform_later).with(SyncJob::SyncType::USER, user.id) + + post :sync + + expect(response).to be_successful + end + end +end diff --git a/spec/controllers/v1/webhooks_controller_spec.rb b/spec/controllers/v1/webhooks_controller_spec.rb new file mode 100644 index 00000000..33f11d57 --- /dev/null +++ b/spec/controllers/v1/webhooks_controller_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe V1::WebhooksController, type: :controller do + let(:user) { FactoryBot.create(:user, otp_required_for_login: true) } + let(:token) { 'token' } + let(:server_provider) { FactoryBot.create(:p4_server_provider, listener_token: 'token') } + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: user) } + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: user) } + + before do + sign_in(user) + allow_any_instance_of(P4ServerProvider).to receive(:commit_info_from_webhook).and_return(commit_info) + end + + describe 'POST receive' do + let(:params) do + { + token: token, + change_root: 'root', + username: user.email, + } + end + let(:commit_info) do + { + email: user.email, + repository_name: repository.name, + sha: 'sha', + ref: 'ref', + } + end + + context 'when all parameters are provided and correct' do + let(:trigger_webhooks) { double } + + it 'triggers webhooks' do + expect(TriggerWebhooks).to receive(:new).and_return(trigger_webhooks) + expect(trigger_webhooks).to receive(:call) + + post :receive, params: params + + expect(response).to be_successful + end + end + + context 'when server is not found' do + let(:token) { 'notoken' } + + it 'returns an error' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_unauthorized + end + end + + context 'when there is no commit info' do + before do + allow_any_instance_of(P4ServerProvider).to receive(:commit_info_from_webhook).and_return(nil) + end + + it 'returns an error' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response.status).to eq(500) + end + end + + context 'when there is no user' do + before do + commit_info[:email] = 'no@email.com' + end + + it 'does not trigger webhooks' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_successful + end + end + + context 'when there is no repository' do + before do + commit_info[:repository_name] = 'NoREPO' + end + + it 'does not trigger webhooks' do + expect(TriggerWebhooks).not_to receive(:new) + + post :receive, params: params + + expect(response).to be_successful + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb new file mode 100644 index 00000000..730d5620 --- /dev/null +++ b/spec/factories.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + name { 'Bob Uncle' } + email { 'bob@uncle.com' } + password { 'TestPass#123' } + confirmed_at { Date.today } + end + + factory :server_provider do + name { 'TestServer' } + url { 'http://test.com/server' } + type { 'P4ServerProvider' } + end + + factory :server_provider_permission do + association :user + association :server_provider + permission { :owner } + end + + factory :p4_server_provider, class: 'P4ServerProvider' do + name { 'TestP4Server' } + url { 'http://test.com/server' } + type { 'P4ServerProvider' } + end + + factory :repository do + name { 'TestRepo' } + url { 'http://test.com/repo' } + association :server_provider + last_synced_at { Time.now } + end + + factory :repository_permission do + association :user + association :repository + permission { :super } + end + + factory :ref do + association :repository + name { 'TestRef' } + type { :branch } + end + + factory :commit do + association :ref + association :repository + association :user + sha { 'COMMIT_SHA' } + message { 'Commit Message' } + committed_at { '2021-09-01 00:00:00' } + end + + factory :webhook do + association :repository + name { 'RepoWebHook' } + url { 'https://webhook.repo/' } + active { true } + insecure_ssl { false } + created_at { '2021-09-01 00:00:00' } + end +end diff --git a/spec/models/p4_server_provider_spec.rb b/spec/models/p4_server_provider_spec.rb new file mode 100644 index 00000000..6e8b9fe6 --- /dev/null +++ b/spec/models/p4_server_provider_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe P4ServerProvider, type: :model do + subject { FactoryBot.create(:p4_server_provider) } + + describe '#bare_repo' do + context 'when username and password passed' do + let(:username) { 'username' } + let(:password) { '1337PASSWORD' } + + it 'uses provided user and password to return bare repo' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(nil, subject.url, username, password).and_call_original + + expect(subject.bare_repo(nil, username, password)).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + + context 'when repository passed' do + let(:username) { 'username' } + let(:token) { 'testtoken' } + let(:repo) { FactoryBot.create(:repository, server_provider: subject, token: token) } + + before do + repo.settings(:p4_host).username = username + end + + it 'uses repo user and token to return bare repo' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(repo, subject.url, username, token).and_call_original + + expect(subject.bare_repo(repo)).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + + context 'when nothing is passed' do + let(:username) { 'username' } + let(:token) { 'testtoken' } + + before do + subject.settings(:p4_host).username = username + subject.token = token + end + + it 'uses server user and token' do + expect(Travis::VcsProxy::Repositories::P4).to receive(:new).with(nil, subject.url, username, token).and_call_original + + expect(subject.bare_repo).to be_instance_of(Travis::VcsProxy::Repositories::P4) + end + end + end + + describe '#remote_repositories' do + it 'returns remote repositories' do + expect(subject).to receive(:bare_repo).and_call_original + expect_any_instance_of(Travis::VcsProxy::Repositories::P4).to receive(:repositories) + + subject.remote_repositories + end + end + + describe '#commit_info_from_webhook' do + context 'when payload does not contain change_root and username' do + it 'return nil' do + expect(subject.commit_info_from_webhook({})).to eq(nil) + end + end + + context 'when payload contains change_root and username' do + let(:payload) { { change_root: 'test', username: 'user' } } + + it 'returns commit info from webhook' do + expect(subject).to receive(:bare_repo).and_call_original + expect_any_instance_of(Travis::VcsProxy::Repositories::P4).to receive(:commit_info).with(payload[:change_root], payload[:username]).and_return('test') + + expect(subject.commit_info_from_webhook(payload)).to eq('test') + end + end + end + + describe '#provider_type' do + it 'returns provider_type' do + expect(subject.provider_type).to eq('perforce') + end + end + + describe '#default_branch' do + it 'returns default_branch' do + expect(subject.default_branch).to eq('master') + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb new file mode 100644 index 00000000..9d16bf1b --- /dev/null +++ b/spec/models/repository_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Repository, type: :model do + subject { FactoryBot.create(:repository, server_provider: server_provider) } + + let(:server_provider) { FactoryBot.create(:server_provider) } + let!(:branch_ref) { FactoryBot.create(:ref, name: 'BranchRef', repository: subject, type: :branch) } + let!(:tag_ref) { FactoryBot.create(:ref, name: 'TagRef', repository: subject, type: :tag) } + + describe '#branches' do + it 'returns branch refs' do + expect(subject.branches).to include(branch_ref) + end + end + + describe '#tags' do + it 'returns tag refs' do + expect(subject.tags).to include(tag_ref) + end + end + + describe '#repo' do + it 'returns bare repo' do + expect(server_provider).to receive(:bare_repo).with(subject, nil, nil) + + subject.repo + end + + it 'authorizes and returns bare repo' do + expect(server_provider).to receive(:bare_repo).with(subject, 'test', 'token') + + subject.repo('test', 'token') + end + end + + describe '#file_contents' do + let(:bare_repo) { double } + let(:ref) { 'ref' } + let(:path) { 'path' } + + it 'returns file contents' do + expect(server_provider).to receive(:bare_repo).with(subject, nil, nil).and_return(bare_repo) + expect(bare_repo).to receive(:file_contents).with(ref, path) + + subject.file_contents(ref, path) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 00000000..4b0bde27 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe User, type: :model do + subject { FactoryBot.create(:user) } + + let(:server_provider) { FactoryBot.create(:server_provider) } + + context 'with server_providers' do + let!(:server_provider_permission) { FactoryBot.create(:server_provider_permission, server_provider: server_provider, user: subject) } + + describe '#server_provider_permission' do + it 'returns permissions for specified server provider' do + result = subject.server_provider_permission(server_provider.id) + + expect(result).to eq(server_provider_permission) + end + end + + describe '#set_server_provider_permission' do + it 'sets permissions for specified server provider' do + subject.set_server_provider_permission(server_provider.id, :member) + + expect(server_provider_permission.reload.permission).to eq('member') + end + end + end + + context 'with repositories' do + let(:repository) { FactoryBot.create(:repository, server_provider: server_provider) } + let!(:repository_permission) { FactoryBot.create(:repository_permission, repository: repository, user: subject) } + + describe '#repository_permission' do + it 'returns permissions for specified repository' do + result = subject.repository_permission(repository.id) + + expect(result).to eq(repository_permission) + end + end + end + + describe '#mark_as_deleted' do + it 'obfuscates user email, name and marks inactive' do + subject.mark_as_deleted + + expect(subject.reload.email).to include('deleted_email_') + expect(subject.name).to eq(nil) + expect(subject.active).to be_falsey + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..6ccdcef8 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,88 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV.delete('DATABASE_URL') +ENV['RACK_ENV'] = ENV['RAILS_ENV'] = ENV['ENV'] = 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +require 'database_cleaner/active_record' + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # add Devise methods + config.include Devise::Test::ControllerHelpers, type: :controller + config.include Devise::Test::IntegrationHelpers, type: :request + + # add `FactoryBot` methods + config.include FactoryBot::Syntax::Methods + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + config.before(:suite) do + FactoryBot.find_definitions + DatabaseCleaner.strategy = :transaction + DatabaseCleaner.clean_with(:truncation) + end + + config.before :each do + DatabaseCleaner.start + end + + config.after :each do + DatabaseCleaner.clean + end + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..ce33d66d --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,96 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 00000000..e69de29b diff --git a/tmp/pids/.keep b/tmp/pids/.keep new file mode 100644 index 00000000..e69de29b diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 00000000..e69de29b