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)
+ %>
+
+
+ <% resource.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+<% 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