├── .codebeatsettings ├── .env.template ├── .env.test ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── deploy_to_dev.yml │ ├── deploy_to_prod.yml │ ├── deploy_to_staging.yml │ └── tests_and_linters.yml ├── .gitignore ├── .reek.yml ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── admin │ ├── admin_users.rb │ ├── dashboard.rb │ └── users.rb ├── assets │ ├── config │ │ └── manifest.js │ ├── javascripts │ │ └── active_admin.js │ └── stylesheets │ │ └── active_admin.scss ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── api_controller.rb │ │ │ ├── passwords_controller.rb │ │ │ ├── registrations_controller.rb │ │ │ ├── sessions_controller.rb │ │ │ ├── token_validations_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ └── concerns │ │ ├── act_as_api_request.rb │ │ ├── exception_handler.rb │ │ └── localizable.rb ├── jobs │ └── application_job.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── admin_user.rb │ ├── application_record.rb │ ├── concerns │ │ └── with_stats_scopes.rb │ └── user.rb ├── services │ └── application_service.rb └── views │ ├── admin │ └── dashboard │ │ └── _sign_ups_per_month.html.erb │ ├── api │ └── v1 │ │ ├── registrations │ │ └── create.json.jb │ │ ├── sessions │ │ └── create.json.jb │ │ ├── token_validations │ │ └── validate.jb │ │ └── users │ │ ├── _minimal.json.jb │ │ ├── show.json.jb │ │ └── update.json.jb │ ├── devise │ └── mailer │ │ ├── reset_password_instructions.html.erb │ │ └── reset_password_instructions.text.erb │ └── layouts │ ├── mailer.html.erb │ └── mailer.text.erb ├── bin ├── bundle ├── rails ├── rake ├── setup └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── credentials │ ├── development.yml.enc │ └── test.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── active_admin.rb │ ├── activeadmin_addons.rb │ ├── application_controller_renderer.rb │ ├── cors.rb │ ├── devise.rb │ ├── devise_token_auth.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── locale.rb │ ├── mime_types.rb │ ├── pagy.rb │ ├── rack_attack.rb │ ├── sentry.rb │ ├── sidekiq.rb │ ├── strong_migrations.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ ├── devise.es.yml │ ├── en.yml │ ├── es.yml │ └── rails.es.yml ├── puma.rb ├── routes.rb ├── sidekiq.yml ├── spring.rb └── storage.yml ├── db ├── migrate │ ├── 20190703155941_create_users.rb │ ├── 20190705213619_devise_token_auth_create_users.rb │ ├── 20200117193722_devise_create_admin_users.rb │ └── 20200309184444_add_locale_to_users.rb ├── schema.rb └── seeds.rb ├── lib ├── gem_extensions │ └── devise │ │ └── token_generator.rb └── tasks │ ├── auto_annotate_models.rake │ └── linters.rake ├── log └── .keep ├── public ├── assets │ └── favicon.png ├── favicon.png └── robots.txt ├── release_tasks.sh ├── rubocop ├── cop │ └── migration │ │ └── add_index.rb ├── migration_helpers.rb ├── rubocop.rb ├── rubocop_rails.yml └── rubocop_rspec.yml ├── spec ├── config │ └── initializers │ │ └── rack │ │ └── attack_spec.rb ├── factories │ ├── admin_users.rb │ └── users.rb ├── mailers │ └── previews │ │ ├── devise_mailer_preview.rb │ │ └── reset_password_preview.rb ├── models │ ├── admin_user_spec.rb │ └── user_spec.rb ├── rails_helper.rb ├── requests │ └── api │ │ └── v1 │ │ ├── passwords │ │ ├── create_spec.rb │ │ ├── edit_spec.rb │ │ └── update_spec.rb │ │ ├── sessions │ │ ├── create_spec.rb │ │ └── destroy_spec.rb │ │ ├── token_validations │ │ └── validate_token_spec.rb │ │ └── users │ │ ├── create_spec.rb │ │ ├── show_spec.rb │ │ └── update_spec.rb ├── rubocop │ └── cop │ │ └── migration │ │ └── add_index_spec.rb ├── spec_helper.rb └── support │ ├── factory_bot.rb │ ├── request_helpers.rb │ ├── shared_context │ ├── .rubocop.yml │ └── request_initializer.rb │ ├── shared_examples │ ├── authentication.rb │ └── have_http_status.rb │ ├── shoulda_matchers.rb │ ├── simple_cov.rb │ └── webmock.rb ├── storage └── .keep ├── tmp └── .keep └── vendor └── .keep /.codebeatsettings: -------------------------------------------------------------------------------- 1 | { 2 | "RUBY": { 3 | "ABC": [17, 20, 40, 60] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SITE_TITLE=Boilerplate 2 | SERVER_URL= 3 | DB_HOST=localhost 4 | DB_USERNAME=postgres 5 | DB_PASSWORD= 6 | DB_NAME=rails-api-boilerplate 7 | REDIS_URL=redis://localhost:6379 8 | MAILER_DOMAIN= 9 | SENDGRID_API_KEY= 10 | DEFAULT_FROM_EMAIL_ADDRESS=no-reply@my-app.com 11 | SENTRY_DSN= 12 | JOB_MONITOR_USERNAME=admin 13 | JOB_MONITOR_PASSWORD=admin 14 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | REDIS_URL=redis://localhost:6379 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @loopstudio/ruby-devs 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | #### :link: Board reference: 2 | 3 | * [NameOnTheCard](linkToTheCard) 4 | 5 | --- 6 | 7 | #### Description: 8 | 9 | Provide a good description of the problem this PR is trying to address. 10 | 11 | --- 12 | 13 | #### :pushpin: Notes: 14 | 15 | * Include TODOS, warnings, things other devs need to be aware of when merging, etc. 16 | 17 | --- 18 | 19 | #### :heavy_check_mark:Tasks: 20 | 21 | * Write the list of things you performed to help the reviewer. 22 | 23 | --- 24 | 25 | @loopstudio/ruby-devs 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: rubocop 11 | versions: 12 | - 1.11.0 13 | - 1.9.0 14 | - dependency-name: rspec-rails 15 | versions: 16 | - 4.1.0 17 | - 5.0.0 18 | - dependency-name: webmock 19 | versions: 20 | - 3.11.3 21 | - 3.12.0 22 | - dependency-name: bootsnap 23 | versions: 24 | - 1.6.0 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: akhileshns/heroku-deploy@v3.0.4 14 | with: 15 | heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | heroku_app_name: ${{secrets.HEROKU_APP}} 17 | heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_dev.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Dev 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - develop 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_DEV_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_prod.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Production 2 | 3 | # on: 4 | # push: 5 | # branches: 6 | # - master 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_PROD_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy_to_staging.yml: -------------------------------------------------------------------------------- 1 | # name: Deploy to Staging 2 | 3 | # on: 4 | # pull_request: 5 | # branches: 6 | # - master 7 | 8 | # jobs: 9 | # build: 10 | # runs-on: ubuntu-latest 11 | # steps: 12 | # - uses: actions/checkout@v2 13 | # - uses: akhileshns/heroku-deploy@v3.0.4 14 | # with: 15 | # heroku_api_key: ${{secrets.HEROKU_API_KEY}} 16 | # heroku_app_name: ${{secrets.HEROKU_STAGING_APP}} 17 | # heroku_email: ${{secrets.HEROKU_EMAIL}} 18 | -------------------------------------------------------------------------------- /.github/workflows/tests_and_linters.yml: -------------------------------------------------------------------------------- 1 | name: Tests and Linters 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'master' 7 | - 'develop' 8 | push: 9 | branches: 10 | - 'master' 11 | - 'develop' 12 | - /^release.*/ 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | services: 19 | postgres: 20 | image: postgres:11.5 21 | ports: ["5432:5432"] 22 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 23 | 24 | env: 25 | PGHOST: localhost 26 | PGUSER: postgres 27 | RAILS_ENV: test 28 | DEFAULT_FROM_EMAIL_ADDRESS: test@loopstudio.dev 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Set up Ruby (.ruby-version) 34 | uses: ruby/setup-ruby@v1 35 | with: 36 | bundler-cache: true 37 | 38 | - name: Install PostgreSQL 11 client 39 | run: | 40 | sudo apt-get -yqq install libpq-dev 41 | 42 | - name: Build App 43 | run: | 44 | gem install bundler 45 | bundle config path vendor/bundle 46 | bundle install --jobs 4 --retry 3 47 | bin/rails db:setup 48 | 49 | - name: Run Linters 50 | run: bundle exec rake linters 51 | 52 | - name: Run Tests 53 | run: bundle exec rspec 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore uploaded files in development. 21 | /storage/* 22 | !/storage/.keep 23 | .byebug_history 24 | 25 | # Ignore master key for decrypting credentials and more. 26 | /config/master.key 27 | 28 | /config/credentials/development.key 29 | 30 | /config/credentials/test.key 31 | 32 | .env 33 | coverage 34 | *.rdb 35 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | detectors: 2 | Attribute: 3 | enabled: true 4 | exclude: [] 5 | BooleanParameter: 6 | enabled: true 7 | exclude: [] 8 | ClassVariable: 9 | enabled: true 10 | exclude: [] 11 | ControlParameter: 12 | enabled: true 13 | exclude: [] 14 | DataClump: 15 | enabled: true 16 | exclude: [] 17 | max_copies: 2 18 | min_clump_size: 2 19 | DuplicateMethodCall: 20 | enabled: true 21 | exclude: [] 22 | max_calls: 1 23 | allow_calls: 24 | - 'Rails.env' 25 | FeatureEnvy: 26 | enabled: true 27 | exclude: 28 | - WithStatsScopes#this_month 29 | InstanceVariableAssumption: 30 | enabled: false 31 | exclude: [] 32 | IrresponsibleModule: 33 | enabled: false 34 | exclude: [] 35 | LongParameterList: 36 | enabled: true 37 | exclude: [] 38 | max_params: 3 39 | overrides: 40 | initialize: 41 | max_params: 5 42 | LongYieldList: 43 | enabled: true 44 | exclude: [] 45 | max_params: 3 46 | ManualDispatch: 47 | enabled: true 48 | exclude: [] 49 | MissingSafeMethod: 50 | enabled: false 51 | exclude: [] 52 | ModuleInitialize: 53 | enabled: true 54 | exclude: [] 55 | NestedIterators: 56 | enabled: true 57 | exclude: [] 58 | max_allowed_nesting: 1 59 | ignore_iterators: 60 | - tap 61 | NilCheck: 62 | enabled: false 63 | exclude: [] 64 | RepeatedConditional: 65 | enabled: true 66 | exclude: [] 67 | max_ifs: 2 68 | SubclassedFromCoreClass: 69 | enabled: true 70 | exclude: [] 71 | TooManyConstants: 72 | enabled: true 73 | exclude: [] 74 | max_constants: 5 75 | TooManyInstanceVariables: 76 | enabled: true 77 | exclude: [] 78 | max_instance_variables: 4 79 | TooManyMethods: 80 | enabled: true 81 | exclude: [] 82 | max_methods: 15 83 | TooManyStatements: 84 | enabled: true 85 | exclude: 86 | - initialize 87 | max_statements: 9 88 | UncommunicativeMethodName: 89 | enabled: true 90 | exclude: [] 91 | reject: 92 | - "/^[a-z]$/" 93 | - "/[0-9]$/" 94 | - "/[A-Z]/" 95 | accept: [] 96 | UncommunicativeModuleName: 97 | enabled: true 98 | exclude: [] 99 | reject: 100 | - "/^.$/" 101 | - "/[0-9]$/" 102 | accept: 103 | - "/V[0-9]/" 104 | UncommunicativeParameterName: 105 | enabled: true 106 | exclude: [] 107 | reject: 108 | - "/^.$/" 109 | - "/[0-9]$/" 110 | - "/[A-Z]/" 111 | - "/^_/" 112 | accept: [] 113 | UncommunicativeVariableName: 114 | enabled: true 115 | exclude: [e] 116 | reject: 117 | - "/^.$/" 118 | - "/[0-9]$/" 119 | - "/[A-Z]/" 120 | accept: 121 | - "/^_$/" 122 | UnusedParameters: 123 | enabled: true 124 | exclude: [] 125 | UnusedPrivateMethod: 126 | enabled: false 127 | exclude: [] 128 | UtilityFunction: 129 | enabled: true 130 | exclude: 131 | - Devise::TokenGenerator 132 | public_methods_only: false 133 | 134 | directories: 135 | "app/services": 136 | UtilityFunction: 137 | enabled: false 138 | FeatureEnvy: 139 | enabled: false 140 | "app/presenters": 141 | UtilityFunction: 142 | enabled: false 143 | FeatureEnvy: 144 | enabled: false 145 | "app/jobs": 146 | UtilityFunction: 147 | enabled: false 148 | FeatureEnvy: 149 | enabled: false 150 | "app/uploaders": 151 | UtilityFunction: 152 | enabled: false 153 | "spec/support": 154 | UtilityFunction: 155 | enabled: false 156 | "app/validators": 157 | ControlParameter: 158 | enabled: false 159 | "db/migrate": 160 | UtilityFunction: 161 | enabled: false 162 | FeatureEnvy: 163 | enabled: false 164 | TooManyStatements: 165 | enabled: false 166 | "spec/mailers/previews": 167 | UtilityFunction: 168 | enabled: false 169 | "rubocop": 170 | UtilityFunction: 171 | enabled: false 172 | FeatureEnvy: 173 | enabled: false 174 | "app/admin": 175 | FeatureEnvy: 176 | enabled: false 177 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require rails_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | - ./rubocop/rubocop_rails.yml 4 | - ./rubocop/rubocop_rspec.yml 5 | 6 | inherit_mode: 7 | merge: 8 | - Exclude 9 | 10 | require: 11 | - rubocop-rails 12 | - rubocop-rspec 13 | - ./rubocop/rubocop 14 | 15 | AllCops: 16 | NewCops: enable 17 | Exclude: 18 | - vendor/**/* 19 | - .circleci/* 20 | - db/migrate/* 21 | - db/schema.rb 22 | - bin/bundle 23 | - rubocop/**/*.rb 24 | 25 | Layout/LineLength: 26 | Max: 100 27 | Exclude: 28 | - config/**/* 29 | 30 | Metrics/BlockLength: 31 | Exclude: 32 | - config/**/* 33 | - spec/**/* 34 | - lib/tasks/auto_annotate_models.rake 35 | - app/admin/**/* 36 | 37 | Style/BlockDelimiters: 38 | EnforcedStyle: braces_for_chaining 39 | 40 | Style/Documentation: 41 | Enabled: false 42 | 43 | Style/FrozenStringLiteralComment: 44 | Enabled: false 45 | 46 | Style/NumericLiterals: 47 | Exclude: 48 | - config/initializers/strong_migrations.rb 49 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-02-03 19:19:09 UTC using RuboCop version 1.9.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 9 10 | # Configuration parameters: Max. 11 | RSpec/ExampleLength: 12 | Exclude: 13 | - 'spec/requests/api/v1/sessions/create_spec.rb' 14 | - 'spec/requests/api/v1/token_validations/validate_token_spec.rb' 15 | - 'spec/requests/api/v1/users/create_spec.rb' 16 | - 'spec/requests/api/v1/users/show_spec.rb' 17 | - 'spec/requests/api/v1/users/update_spec.rb' 18 | - 'spec/rubocop/cop/migration/add_index_spec.rb' 19 | 20 | # Offense count: 4 21 | RSpec/NestedGroups: 22 | Max: 5 23 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.7.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.7.2' 3 | 4 | gem 'bootsnap', '>= 1.4.2', require: false 5 | gem 'rails', '~> 6.1.6' 6 | 7 | # WebServer 8 | gem 'puma', '~> 5.6' 9 | gem 'rack', '~> 2.2.3' 10 | gem 'rack-attack', '~> 6.6.1' 11 | gem 'rack-cors', '~> 1.1.1' 12 | 13 | # Database 14 | gem 'pg', '~> 1.4.1' 15 | gem 'strong_migrations', '~> 1.2.0' 16 | 17 | # Environment variables 18 | gem 'dotenv-rails', '~> 2.7.6' 19 | 20 | # Async worker 21 | gem 'sidekiq', '~> 6.5.1' 22 | 23 | # Nullify blank strings before saving to db 24 | gem 'nilify_blanks', '~> 1.4' 25 | 26 | # Backoffice 27 | gem 'activeadmin', '~> 2.13.1' 28 | gem 'activeadmin_addons', '~> 1.9.0' 29 | gem 'active_admin_theme', '~> 1.1' 30 | gem 'chartkick', '~> 4.2.0' 31 | gem 'groupdate', '~> 6.1.0' 32 | gem 'ransack', '~> 3.2.1' 33 | 34 | # Authentication 35 | gem 'devise', '~> 4.8.1' 36 | gem 'devise_token_auth', '~> 1.2.0' 37 | 38 | # Serializing json views 39 | gem 'jb', '~> 0.8.0' 40 | 41 | # Pagination 42 | gem 'pagy', '~> 5.10' 43 | 44 | # Monitoring errors 45 | gem 'sentry-rails' 46 | gem 'sentry-ruby' 47 | gem 'sentry-sidekiq' 48 | 49 | group :development, :test do 50 | gem 'bullet', '~> 7.0.2' 51 | gem 'byebug', '>= 11.0.1', platforms: %i[mri mingw x64_mingw] 52 | gem 'factory_bot_rails', '~> 6.2.0' 53 | gem 'faker', '~> 2.21.0' 54 | gem 'rspec-rails', '~> 5.1.2' 55 | end 56 | 57 | group :development do 58 | gem 'annotate', '~> 3.2.0' 59 | gem 'letter_opener', '~> 1.8.1' 60 | gem 'listen', '>= 3.0.5', '< 3.8' 61 | gem 'reek', '~> 6.1.1', require: false 62 | gem 'rubocop', '~> 1.30.1', require: false 63 | gem 'rubocop-rails', '~> 2.15.0', require: false 64 | gem 'rubocop-rspec', '~> 2.11.1', require: false 65 | gem 'spring', '~> 2.1.1' 66 | gem 'spring-watcher-listen', '~> 2.0.0' 67 | end 68 | 69 | group :test do 70 | gem 'rspec-json_expectations', '~> 2.2.0' 71 | gem 'shoulda-matchers', '~> 5.1.0' 72 | gem 'simplecov', '~> 0.21.2' 73 | gem 'webmock', '~> 3.14.0' 74 | end 75 | 76 | gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] 77 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (6.1.6) 5 | actionpack (= 6.1.6) 6 | activesupport (= 6.1.6) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (6.1.6) 10 | actionpack (= 6.1.6) 11 | activejob (= 6.1.6) 12 | activerecord (= 6.1.6) 13 | activestorage (= 6.1.6) 14 | activesupport (= 6.1.6) 15 | mail (>= 2.7.1) 16 | actionmailer (6.1.6) 17 | actionpack (= 6.1.6) 18 | actionview (= 6.1.6) 19 | activejob (= 6.1.6) 20 | activesupport (= 6.1.6) 21 | mail (~> 2.5, >= 2.5.4) 22 | rails-dom-testing (~> 2.0) 23 | actionpack (6.1.6) 24 | actionview (= 6.1.6) 25 | activesupport (= 6.1.6) 26 | rack (~> 2.0, >= 2.0.9) 27 | rack-test (>= 0.6.3) 28 | rails-dom-testing (~> 2.0) 29 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 30 | actiontext (6.1.6) 31 | actionpack (= 6.1.6) 32 | activerecord (= 6.1.6) 33 | activestorage (= 6.1.6) 34 | activesupport (= 6.1.6) 35 | nokogiri (>= 1.8.5) 36 | actionview (6.1.6) 37 | activesupport (= 6.1.6) 38 | builder (~> 3.1) 39 | erubi (~> 1.4) 40 | rails-dom-testing (~> 2.0) 41 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 42 | active_admin_theme (1.1.4) 43 | active_material (1.5.2) 44 | activeadmin (2.13.1) 45 | arbre (~> 1.2, >= 1.2.1) 46 | formtastic (>= 3.1, < 5.0) 47 | formtastic_i18n (~> 0.4) 48 | inherited_resources (~> 1.7) 49 | jquery-rails (~> 4.2) 50 | kaminari (~> 1.0, >= 1.2.1) 51 | railties (>= 6.1, < 7.1) 52 | ransack (>= 2.1.1, < 4) 53 | activeadmin_addons (1.9.0) 54 | active_material 55 | railties 56 | require_all 57 | sassc 58 | sassc-rails 59 | xdan-datetimepicker-rails (~> 2.5.1) 60 | activejob (6.1.6) 61 | activesupport (= 6.1.6) 62 | globalid (>= 0.3.6) 63 | activemodel (6.1.6) 64 | activesupport (= 6.1.6) 65 | activerecord (6.1.6) 66 | activemodel (= 6.1.6) 67 | activesupport (= 6.1.6) 68 | activestorage (6.1.6) 69 | actionpack (= 6.1.6) 70 | activejob (= 6.1.6) 71 | activerecord (= 6.1.6) 72 | activesupport (= 6.1.6) 73 | marcel (~> 1.0) 74 | mini_mime (>= 1.1.0) 75 | activesupport (6.1.6) 76 | concurrent-ruby (~> 1.0, >= 1.0.2) 77 | i18n (>= 1.6, < 2) 78 | minitest (>= 5.1) 79 | tzinfo (~> 2.0) 80 | zeitwerk (~> 2.3) 81 | addressable (2.8.0) 82 | public_suffix (>= 2.0.2, < 5.0) 83 | annotate (3.2.0) 84 | activerecord (>= 3.2, < 8.0) 85 | rake (>= 10.4, < 14.0) 86 | arbre (1.5.0) 87 | activesupport (>= 3.0.0, < 7.1) 88 | ruby2_keywords (>= 0.0.2, < 1.0) 89 | ast (2.4.2) 90 | bcrypt (3.1.16) 91 | bootsnap (1.12.0) 92 | msgpack (~> 1.2) 93 | builder (3.2.4) 94 | bullet (7.0.2) 95 | activesupport (>= 3.0.0) 96 | uniform_notifier (~> 1.11) 97 | byebug (11.1.3) 98 | chartkick (4.2.0) 99 | concurrent-ruby (1.1.10) 100 | connection_pool (2.2.5) 101 | crack (0.4.5) 102 | rexml 103 | crass (1.0.6) 104 | devise (4.8.1) 105 | bcrypt (~> 3.0) 106 | orm_adapter (~> 0.1) 107 | railties (>= 4.1.0) 108 | responders 109 | warden (~> 1.2.3) 110 | devise_token_auth (1.2.0) 111 | bcrypt (~> 3.0) 112 | devise (> 3.5.2, < 5) 113 | rails (>= 4.2.0, < 6.2) 114 | diff-lcs (1.5.0) 115 | docile (1.3.4) 116 | dotenv (2.7.6) 117 | dotenv-rails (2.7.6) 118 | dotenv (= 2.7.6) 119 | railties (>= 3.2) 120 | erubi (1.10.0) 121 | factory_bot (6.2.0) 122 | activesupport (>= 5.0.0) 123 | factory_bot_rails (6.2.0) 124 | factory_bot (~> 6.2.0) 125 | railties (>= 5.0.0) 126 | faker (2.21.0) 127 | i18n (>= 1.8.11, < 2) 128 | ffi (1.15.5) 129 | formtastic (4.0.0) 130 | actionpack (>= 5.2.0) 131 | formtastic_i18n (0.7.0) 132 | globalid (1.0.0) 133 | activesupport (>= 5.0) 134 | groupdate (6.1.0) 135 | activesupport (>= 5.2) 136 | has_scope (0.8.0) 137 | actionpack (>= 5.2) 138 | activesupport (>= 5.2) 139 | hashdiff (1.0.1) 140 | i18n (1.10.0) 141 | concurrent-ruby (~> 1.0) 142 | inherited_resources (1.13.1) 143 | actionpack (>= 5.2, < 7.1) 144 | has_scope (~> 0.6) 145 | railties (>= 5.2, < 7.1) 146 | responders (>= 2, < 4) 147 | jb (0.8.0) 148 | jquery-rails (4.4.0) 149 | rails-dom-testing (>= 1, < 3) 150 | railties (>= 4.2.0) 151 | thor (>= 0.14, < 2.0) 152 | kaminari (1.2.2) 153 | activesupport (>= 4.1.0) 154 | kaminari-actionview (= 1.2.2) 155 | kaminari-activerecord (= 1.2.2) 156 | kaminari-core (= 1.2.2) 157 | kaminari-actionview (1.2.2) 158 | actionview 159 | kaminari-core (= 1.2.2) 160 | kaminari-activerecord (1.2.2) 161 | activerecord 162 | kaminari-core (= 1.2.2) 163 | kaminari-core (1.2.2) 164 | kwalify (0.7.2) 165 | launchy (2.5.0) 166 | addressable (~> 2.7) 167 | letter_opener (1.8.1) 168 | launchy (>= 2.2, < 3) 169 | listen (3.7.1) 170 | rb-fsevent (~> 0.10, >= 0.10.3) 171 | rb-inotify (~> 0.9, >= 0.9.10) 172 | loofah (2.18.0) 173 | crass (~> 1.0.2) 174 | nokogiri (>= 1.5.9) 175 | mail (2.7.1) 176 | mini_mime (>= 0.1.1) 177 | marcel (1.0.2) 178 | method_source (1.0.0) 179 | mini_mime (1.1.2) 180 | mini_portile2 (2.8.0) 181 | minitest (5.15.0) 182 | msgpack (1.5.2) 183 | nilify_blanks (1.4.0) 184 | activerecord (>= 4.0.0) 185 | activesupport (>= 4.0.0) 186 | nio4r (2.5.8) 187 | nokogiri (1.13.6) 188 | mini_portile2 (~> 2.8.0) 189 | racc (~> 1.4) 190 | orm_adapter (0.5.0) 191 | pagy (5.10.1) 192 | activesupport 193 | parallel (1.22.1) 194 | parser (3.1.2.0) 195 | ast (~> 2.4.1) 196 | pg (1.4.1) 197 | public_suffix (4.0.6) 198 | puma (5.6.4) 199 | nio4r (~> 2.0) 200 | racc (1.6.0) 201 | rack (2.2.3.1) 202 | rack-attack (6.6.1) 203 | rack (>= 1.0, < 3) 204 | rack-cors (1.1.1) 205 | rack (>= 2.0.0) 206 | rack-test (1.1.0) 207 | rack (>= 1.0, < 3) 208 | rails (6.1.6) 209 | actioncable (= 6.1.6) 210 | actionmailbox (= 6.1.6) 211 | actionmailer (= 6.1.6) 212 | actionpack (= 6.1.6) 213 | actiontext (= 6.1.6) 214 | actionview (= 6.1.6) 215 | activejob (= 6.1.6) 216 | activemodel (= 6.1.6) 217 | activerecord (= 6.1.6) 218 | activestorage (= 6.1.6) 219 | activesupport (= 6.1.6) 220 | bundler (>= 1.15.0) 221 | railties (= 6.1.6) 222 | sprockets-rails (>= 2.0.0) 223 | rails-dom-testing (2.0.3) 224 | activesupport (>= 4.2.0) 225 | nokogiri (>= 1.6) 226 | rails-html-sanitizer (1.4.2) 227 | loofah (~> 2.3) 228 | railties (6.1.6) 229 | actionpack (= 6.1.6) 230 | activesupport (= 6.1.6) 231 | method_source 232 | rake (>= 12.2) 233 | thor (~> 1.0) 234 | rainbow (3.1.1) 235 | rake (13.0.6) 236 | ransack (3.2.1) 237 | activerecord (>= 6.1.5) 238 | activesupport (>= 6.1.5) 239 | i18n 240 | rb-fsevent (0.11.0) 241 | rb-inotify (0.10.1) 242 | ffi (~> 1.0) 243 | redis (4.6.0) 244 | reek (6.1.1) 245 | kwalify (~> 0.7.0) 246 | parser (~> 3.1.0) 247 | rainbow (>= 2.0, < 4.0) 248 | regexp_parser (2.5.0) 249 | require_all (3.0.0) 250 | responders (3.0.1) 251 | actionpack (>= 5.0) 252 | railties (>= 5.0) 253 | rexml (3.2.5) 254 | rspec-core (3.11.0) 255 | rspec-support (~> 3.11.0) 256 | rspec-expectations (3.11.0) 257 | diff-lcs (>= 1.2.0, < 2.0) 258 | rspec-support (~> 3.11.0) 259 | rspec-json_expectations (2.2.0) 260 | rspec-mocks (3.11.1) 261 | diff-lcs (>= 1.2.0, < 2.0) 262 | rspec-support (~> 3.11.0) 263 | rspec-rails (5.1.2) 264 | actionpack (>= 5.2) 265 | activesupport (>= 5.2) 266 | railties (>= 5.2) 267 | rspec-core (~> 3.10) 268 | rspec-expectations (~> 3.10) 269 | rspec-mocks (~> 3.10) 270 | rspec-support (~> 3.10) 271 | rspec-support (3.11.0) 272 | rubocop (1.30.1) 273 | parallel (~> 1.10) 274 | parser (>= 3.1.0.0) 275 | rainbow (>= 2.2.2, < 4.0) 276 | regexp_parser (>= 1.8, < 3.0) 277 | rexml (>= 3.2.5, < 4.0) 278 | rubocop-ast (>= 1.18.0, < 2.0) 279 | ruby-progressbar (~> 1.7) 280 | unicode-display_width (>= 1.4.0, < 3.0) 281 | rubocop-ast (1.18.0) 282 | parser (>= 3.1.1.0) 283 | rubocop-rails (2.15.0) 284 | activesupport (>= 4.2.0) 285 | rack (>= 1.1) 286 | rubocop (>= 1.7.0, < 2.0) 287 | rubocop-rspec (2.11.1) 288 | rubocop (~> 1.19) 289 | ruby-progressbar (1.11.0) 290 | ruby2_keywords (0.0.5) 291 | sassc (2.4.0) 292 | ffi (~> 1.9) 293 | sassc-rails (2.1.2) 294 | railties (>= 4.0.0) 295 | sassc (>= 2.0) 296 | sprockets (> 3.0) 297 | sprockets-rails 298 | tilt 299 | sentry-rails (5.3.1) 300 | railties (>= 5.0) 301 | sentry-ruby-core (~> 5.3.1) 302 | sentry-ruby (5.3.1) 303 | concurrent-ruby (~> 1.0, >= 1.0.2) 304 | sentry-ruby-core (= 5.3.1) 305 | sentry-ruby-core (5.3.1) 306 | concurrent-ruby 307 | sentry-sidekiq (5.3.1) 308 | sentry-ruby-core (~> 5.3.1) 309 | sidekiq (>= 3.0) 310 | shoulda-matchers (5.1.0) 311 | activesupport (>= 5.2.0) 312 | sidekiq (6.5.1) 313 | connection_pool (>= 2.2.2) 314 | rack (~> 2.0) 315 | redis (>= 4.2.0) 316 | simplecov (0.21.2) 317 | docile (~> 1.1) 318 | simplecov-html (~> 0.11) 319 | simplecov_json_formatter (~> 0.1) 320 | simplecov-html (0.12.3) 321 | simplecov_json_formatter (0.1.2) 322 | spring (2.1.1) 323 | spring-watcher-listen (2.0.1) 324 | listen (>= 2.7, < 4.0) 325 | spring (>= 1.2, < 3.0) 326 | sprockets (4.0.3) 327 | concurrent-ruby (~> 1.0) 328 | rack (> 1, < 3) 329 | sprockets-rails (3.4.2) 330 | actionpack (>= 5.2) 331 | activesupport (>= 5.2) 332 | sprockets (>= 3.0.0) 333 | strong_migrations (1.2.0) 334 | activerecord (>= 5.2) 335 | thor (1.2.1) 336 | tilt (2.0.10) 337 | tzinfo (2.0.4) 338 | concurrent-ruby (~> 1.0) 339 | unicode-display_width (2.1.0) 340 | uniform_notifier (1.16.0) 341 | warden (1.2.9) 342 | rack (>= 2.0.9) 343 | webmock (3.14.0) 344 | addressable (>= 2.8.0) 345 | crack (>= 0.3.2) 346 | hashdiff (>= 0.4.0, < 2.0.0) 347 | websocket-driver (0.7.5) 348 | websocket-extensions (>= 0.1.0) 349 | websocket-extensions (0.1.5) 350 | xdan-datetimepicker-rails (2.5.4) 351 | jquery-rails 352 | rails (>= 3.2.16) 353 | zeitwerk (2.6.0) 354 | 355 | PLATFORMS 356 | ruby 357 | 358 | DEPENDENCIES 359 | active_admin_theme (~> 1.1) 360 | activeadmin (~> 2.13.1) 361 | activeadmin_addons (~> 1.9.0) 362 | annotate (~> 3.2.0) 363 | bootsnap (>= 1.4.2) 364 | bullet (~> 7.0.2) 365 | byebug (>= 11.0.1) 366 | chartkick (~> 4.2.0) 367 | devise (~> 4.8.1) 368 | devise_token_auth (~> 1.2.0) 369 | dotenv-rails (~> 2.7.6) 370 | factory_bot_rails (~> 6.2.0) 371 | faker (~> 2.21.0) 372 | groupdate (~> 6.1.0) 373 | jb (~> 0.8.0) 374 | letter_opener (~> 1.8.1) 375 | listen (>= 3.0.5, < 3.8) 376 | nilify_blanks (~> 1.4) 377 | pagy (~> 5.10) 378 | pg (~> 1.4.1) 379 | puma (~> 5.6) 380 | rack (~> 2.2.3) 381 | rack-attack (~> 6.6.1) 382 | rack-cors (~> 1.1.1) 383 | rails (~> 6.1.6) 384 | ransack (~> 3.2.1) 385 | reek (~> 6.1.1) 386 | rspec-json_expectations (~> 2.2.0) 387 | rspec-rails (~> 5.1.2) 388 | rubocop (~> 1.30.1) 389 | rubocop-rails (~> 2.15.0) 390 | rubocop-rspec (~> 2.11.1) 391 | sentry-rails 392 | sentry-ruby 393 | sentry-sidekiq 394 | shoulda-matchers (~> 5.1.0) 395 | sidekiq (~> 6.5.1) 396 | simplecov (~> 0.21.2) 397 | spring (~> 2.1.1) 398 | spring-watcher-listen (~> 2.0.0) 399 | strong_migrations (~> 1.2.0) 400 | tzinfo-data 401 | webmock (~> 3.14.0) 402 | 403 | RUBY VERSION 404 | ruby 2.7.2p137 405 | 406 | BUNDLED WITH 407 | 2.1.4 408 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Loop Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p $PORT -e $RAILS_ENV 2 | worker: bundle exec sidekiq -C ./config/sidekiq.yml -v 3 | release: bash ./release_tasks.sh 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/15303963/84329647-a19cc180-ab5b-11ea-9469-09606895bbbf.png) 2 | 3 |
Start your next Rails 6 API project in seconds
4 | 5 | ![Github Actions badge](https://github.com/loopstudio/rails-api-boilerplate/workflows/Tests%20and%20Linters/badge.svg) 6 | [![Codebeat badge](https://codebeat.co/badges/eaad967a-50bd-41b9-be06-e59a516cb732)](https://codebeat.co/a/loopstudio/projects/github-com-loopstudio-rails-api-boilerplate-master) 7 | 8 | Created and maintained with ❤️ by LoopStudio 9 | 10 |
A foundation with a focus on performance and best practices
11 | 12 | ## Table of Contents 13 | 14 | - [Main Characteristics](#main-characteristics) 15 | - [Gems](#gems) 16 | - [Getting Started](#getting-started) 17 | - [Code quality](#code-quality) 18 | - [Contributing](#contributing) 19 | - [License](#license) 20 | 21 | ## Main Characteristics 22 | 23 | - Language: Ruby 2.7.2+ 24 | - Framework: Rails 6.0.3+ 25 | - Webserver: Puma 26 | - Test Framework: RSpec 27 | - Databases: Postgres & Redis 28 | - Async Processor: Sidekiq 29 | 30 | ## Other Gems 31 | 32 | #### dotenv-rails 33 | For environment variables 34 | 35 | #### Devise 36 | We use [devise](https://github.com/plataformatec/devise) for authentication 37 | 38 | #### Jb 39 | For building API json views 40 | 41 | #### ActiveAdmin 42 | To build quick superadmin back-office features. 43 | 44 | #### Pagy 45 | For those endpoints that need pagination, you should add on the controller method, for example: 46 | ```ruby 47 | pagy, records = pagy(User.all) 48 | pagy_headers_merge(pagy) 49 | render json: records 50 | ``` 51 | The frontend needs to pass the query param `page` to retrieve a specific page. 52 | 53 | #### Nilify Blanks 54 | The [nilify_blanks](https://github.com/rubiety/nilify_blanks) line on `ApplicationRecord` adds a before validation callback to substitute all blank string fields to be `nil` instead, to ensure no blank fields are saved on the db. 55 | 56 | ## Getting Started 57 | 58 | 1. Make sure that you have Rails 6, PostgreSQL, Redis, git cli, and bundle installed. 59 | 2. Clone this repo using `git clone --depth=1 https://github.com/LoopStudio/rails-api-boilerplate.git ` 60 | 3. Update the values of the `.env.template` file to match your app 61 | 4. Create your `.env` file. You have an example at `.env.template`. You should be able to copy it and set your values. 62 | _It's a good practice to keep the `.env.template` updated every time you need a new environment variable._ 63 | 5. Run `bundle install` 64 | 6. Run `bundle exec rake db:create` 65 | 7. Run `bundle exec rake db:migrate` 66 | 8. Run `bundle exec rake db:seed` 67 | 9. Check the test are passing running `rspec` 68 | _At this point you can run `rails s` and start making your REST API calls at `http://localhost:3000`_ 69 | 10. Edit or delete the `CODEOWNERS` file in `.github` directory 70 | 11. Edit this README file to match your project title and description 71 | _It's a good practice to keep this file updated as you make important changes to the installation instructions or project characteristics._ 72 | 12. Delete the `.github/workflows/deploy.yml` file, and uncomment the other workflows or configure your continuous deployment workflow since you might use different environments. 73 | 13. Modify the `.github/CODEOWNERS` file 74 | 75 | ## Tests 76 | 77 | You can run the unit tests with `rspec` or `rspec` followed by a specific test file or directory. 78 | 79 | ## Code Quality 80 | 81 | With `rake linters` you can run the code analysis tool, you can omit rules with: 82 | 83 | - [Rubocop](https://github.com/bbatsov/rubocop/blob/master/config/default.yml) Edit `.rubocop.yml` 84 | 85 | When you update RuboCop version, sometimes you need to change `.rubocop.yml`. If you use [mry](https://github.com/pocke/mry), you can update `.rubocop.yml` to the latest version automatically. 86 | 87 | - [Reek](https://github.com/troessner/reek#configuration-file) Edit `config.reek` 88 | 89 | Pass the `-a` option to auto-fix (only for some linterns). 90 | 91 | ## Job Monitor 92 | 93 | Once the app is up and running, the route `/jobmonitor` will take you to the Sidekiq dashboard so you can see the status of the jobs. 94 | This requires authentication only in production environments. 95 | 96 | **Default Job Monitor Credentials:** 97 | * Username: admin 98 | * Password: admin 99 | 100 | You change them to safer credentials by changing the env vars `JOB_MONITOR_USERNAME` and `JOB_MONITOR_PASSWORD`. 101 | 102 | ## Backoffice 103 | 104 | Once the app is up and running, the route `/admin` will take you to the back-office built using ActiveAdmin. 105 | The first Admin User is created when seeding if there isn't one created already. 106 | 107 | **First Admin User Credentials:** 108 | * Email: admin@example.com 109 | * Password: password 110 | 111 | For other environments other than development make sure you modify it to have safer credentials through the Rails console. 112 | 113 | Once you log in as an Admin User you can manage other Admin Users from there. 114 | 115 | You can change the Backoffice favico (tab icon) on `public/assets/` and match the filename on `config/initializers/active_admin.rb`. 116 | 117 | ## Continuous Deployment 118 | 119 | **This boilerplate contains commented out code for a quick Continuous Deployment setup using Github actions and deploying to the Heroku platform.** 120 | 121 | *(If you are not using those two tools you might simply want to remove the workflows directory and disregard the rest of these instructions.)* 122 | 123 | Since we are used to using git-flow for branching and having **three different environments (dev, staging, and production)**, this boilerplate includes three commented out files on the `.github/workflows` folder so that, when using this repo for an actual project, you can keep these environments updated simply by doing a proper use of the branches. 124 | 125 | * **Deploy to dev**: Triggered every time `develop` branch gets pushed to. For instance, whenever a new feature branch gets merged into the develop branch. 126 | 127 | * **Deploy to staging**: Triggered every time somebody creates (or updates) a Pull Request to master. We usually call these branches using the format: `release/vx.y.z` but it will work regardless of the branch name. We create a release Pull Request at the end of each sprint to deploy to staging the new set of changes, and we leave the Pull Request `On Hold` until we are ready to ship to production. 128 | 129 | * **Deploy to production**: Once the staging changes are approved by the Product Owner, we merge the release branch Pull Request into master, triggering a push on the master branch which deploys to production. 130 | 131 | For this to work you will need the configure some Secrets on your GitHub repository. To add these go to your Github project, click on `Settings`, and then `Secrets`. 132 | 133 | You need to add the following Secrets: 134 | 135 | * **HEROKU_EMAIL**: Email of the owner account of the Heroku apps. 136 | * **HEROKU_API_KEY**: API Key of the owner account of the Heroku apps. Get it by going to your Heroku account, `Account Settings`, and scroll down to reveal the `API KEY`. 137 | * **HEROKU_DEV_APP**: Name of the development app. Eg. `my-project-develop-api` 138 | * **HEROKU_PROD_APP**: Name of the production app. Eg. `my-project-api` 139 | * **HEROKU_STAGING_APP**: Name of the staging app. Eg. `my-project-staging-api` 140 | 141 | ### Notes on Continuous Deployment 142 | 143 | * You can disregard and remove the `deploy.yml` file since we use it to deploy the boilerplate code itself as we work on it, but it will probably be useless to you once you clone this repo for your real-world use case. 144 | 145 | * If you use a different branching strategy or different environments layout, simply delete the files under the workflows directory and set up your own. 146 | 147 | ### Password reset flow 148 | 149 | * Request for a password reset at `/users/password` with the following body: 150 | ```json 151 | { 152 | "email": "" 153 | } 154 | ``` 155 | Where `` should be a registered email or you will get the corresponding message on the response. 156 | 157 | * An email is sent to that address with a 6 digit code. 158 | * With a `GET` request to `users/password/edit?reset_password_token=` you can verify the token validity. 159 | * And to change the password you should send a `PUT` request to `users/password` with the following body: 160 | ```json 161 | { 162 | "reset_password_token": "", 163 | "password": "" 164 | } 165 | ``` 166 | 167 | ## Contributing 168 | 169 | If you've noticed a bug or find something that needs to be refactored, feel free to open an issue or even better, a Pull Request! 170 | 171 | ## License 172 | 173 | This project is licensed under the MIT license. 174 | 175 | Copyright (c) 2020 LoopStudio. 176 | 177 | For more information see [`LICENSE`](LICENSE) 178 | 179 | -------- 180 | 181 | [](https://loopstudio.dev) 182 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/admin/admin_users.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register AdminUser do 2 | permit_params :email, :password, :password_confirmation 3 | 4 | index do 5 | selectable_column 6 | id_column 7 | column :email 8 | column :current_sign_in_at 9 | column :sign_in_count 10 | column :created_at 11 | actions 12 | end 13 | 14 | filter :email 15 | filter :current_sign_in_at 16 | filter :sign_in_count 17 | filter :created_at 18 | 19 | form do |f| 20 | f.inputs do 21 | f.input :email 22 | f.input :password 23 | f.input :password_confirmation 24 | end 25 | f.actions 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/admin/dashboard.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register_page 'Dashboard' do 2 | menu priority: 1, label: proc { I18n.t('active_admin.dashboard') } 3 | 4 | content title: proc { I18n.t('active_admin.dashboard') } do 5 | div class: 'blank_slate_container', id: 'dashboard_default_message' do 6 | span class: 'blank_slate' do 7 | span I18n.t('active_admin.dashboard_welcome.welcome') 8 | small I18n.t('active_admin.dashboard_welcome.call_to_action') 9 | end 10 | end 11 | 12 | def print_stats(relation) 13 | total = relation.count 14 | this_month = relation.extending(WithStatsScopes).this_month.count 15 | para("#{I18n.t('active_admin.total')}: #{total}") 16 | para("#{I18n.t('active_admin.this_month')}: #{this_month}") 17 | end 18 | 19 | columns do 20 | column do 21 | panel I18n.t('activerecord.models.user.other') do 22 | print_stats(User) 23 | end 24 | 25 | panel I18n.t('active_admin.charts.sign_ups_per_month') do 26 | render 'sign_ups_per_month', { users: User.group_by_month(:created_at).count } 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/admin/users.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.register User do 2 | permit_params :email, :first_name, :last_name, :password 3 | 4 | actions :all 5 | 6 | index do 7 | selectable_column 8 | id_column 9 | column :email 10 | column :first_name 11 | column :last_name 12 | 13 | actions 14 | end 15 | 16 | filter :email 17 | filter :first_name 18 | filter :last_name 19 | filter :created_at 20 | 21 | show do 22 | tabs do 23 | tab I18n.t('active_admin.overview') do 24 | panel I18n.t('active_admin.general') do 25 | attributes_table_for user do 26 | row :id 27 | row :email 28 | row :first_name 29 | row :last_name 30 | end 31 | end 32 | 33 | panel I18n.t('active_admin.session_info') do 34 | attributes_table_for user do 35 | row :provider 36 | row :sign_in_count 37 | row :last_sign_in 38 | row :created_at 39 | row :updated_at 40 | end 41 | end 42 | end 43 | end 44 | end 45 | 46 | form do |f| 47 | f.inputs do 48 | if f.object.new_record? 49 | f.input :email 50 | f.input :first_name 51 | f.input :last_name 52 | else 53 | f.input :email, input_html: { disabled: true } 54 | end 55 | f.input :password 56 | end 57 | 58 | actions 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_directory ../javascripts .js 2 | //= link_directory ../stylesheets .css 3 | -------------------------------------------------------------------------------- /app/assets/javascripts/active_admin.js: -------------------------------------------------------------------------------- 1 | //= require active_admin/base 2 | //= require chartkick 3 | //= require Chart.bundle 4 | -------------------------------------------------------------------------------- /app/assets/stylesheets/active_admin.scss: -------------------------------------------------------------------------------- 1 | // SASS variable overrides must be declared before loading up Active Admin's styles. 2 | // 3 | // To view the variables that Active Admin provides, take a look at 4 | // `app/assets/stylesheets/active_admin/mixins/_variables.scss` in the 5 | // Active Admin source. 6 | // 7 | // For example, to change the sidebar width: 8 | // $sidebar-width: 242px; 9 | 10 | // Active Admin's got SASS! 11 | @import "active_admin/mixins"; 12 | @import "active_admin/base"; 13 | @import "wigu/active_admin_theme"; 14 | 15 | // Overriding any non-variable SASS must be done after the fact. 16 | // For example, to change the default status-tag color: 17 | // 18 | // .status_tag { background: #6090DB; } 19 | 20 | a:focus { 21 | outline: none; 22 | } 23 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/api/v1/api_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class ApiController < ActionController::API 4 | include ExceptionHandler 5 | include ActAsApiRequest 6 | include Localizable 7 | include DeviseTokenAuth::Concerns::SetUserByToken 8 | include Pagy::Backend 9 | 10 | before_action :authenticate_user! 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/api/v1/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class PasswordsController < DeviseTokenAuth::PasswordsController 4 | include ExceptionHandler 5 | include ActAsApiRequest 6 | include Localizable 7 | include DeviseTokenAuth::Concerns::SetUserByToken 8 | 9 | before_action :validate_redirect_url_param, only: [] 10 | 11 | def edit 12 | @resource = resource_class.with_reset_password_token(resource_params[:reset_password_token]) 13 | 14 | head(:no_content) && return if @resource&.reset_password_period_valid? 15 | 16 | render_token_invalid 17 | end 18 | 19 | def update 20 | @resource = resource_class.reset_password_by_token(update_params) 21 | errors = @resource.errors 22 | 23 | if errors.empty? 24 | response.headers.merge!(@resource.create_new_auth_token) 25 | render_update_success 26 | else 27 | token_error = errors.messages.key?(:reset_password_token) 28 | token_error ? render_token_invalid : render_attributes_errors(errors) 29 | end 30 | end 31 | 32 | private 33 | 34 | def render_create_success 35 | head(:no_content) 36 | end 37 | 38 | def render_update_success 39 | render('api/v1/users/show', locals: { user: @resource }) 40 | end 41 | 42 | def render_token_invalid 43 | render_errors([I18n.t('errors.invalid_reset_password_token')], :bad_request) 44 | end 45 | 46 | def update_params 47 | params.permit(:reset_password_token, :password, :password_confirmation) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/controllers/api/v1/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class RegistrationsController < DeviseTokenAuth::RegistrationsController 4 | include ExceptionHandler 5 | include ActAsApiRequest 6 | include Localizable 7 | 8 | private 9 | 10 | def sign_up_params 11 | params.require(:user).permit(:email, :password, :first_name, :last_name, :locale) 12 | end 13 | 14 | def render_create_success 15 | render :create 16 | end 17 | 18 | def render_create_error 19 | raise ActiveRecord::RecordInvalid, @resource 20 | end 21 | 22 | def validate_post_data(which, message) 23 | render_errors(message, :bad_request) if which.empty? 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class SessionsController < DeviseTokenAuth::SessionsController 4 | include ExceptionHandler 5 | include ActAsApiRequest 6 | include Localizable 7 | include DeviseTokenAuth::Concerns::SetUserByToken 8 | 9 | before_action :authenticate_user!, only: :destroy 10 | 11 | private 12 | 13 | def resource_params 14 | params.require(:user).permit(:email, :password) 15 | end 16 | 17 | def render_create_success 18 | render(:create) 19 | end 20 | 21 | def render_destroy_success 22 | head(:no_content) 23 | end 24 | 25 | def render_create_error_bad_credentials 26 | render_errors(I18n.t('errors.authentication.invalid_credentials'), :forbidden) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/controllers/api/v1/token_validations_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class TokenValidationsController < DeviseTokenAuth::TokenValidationsController 4 | include ExceptionHandler 5 | include ActAsApiRequest 6 | include Localizable 7 | 8 | def render_validate_token_success 9 | render :validate 10 | end 11 | 12 | def render_validate_token_error 13 | render_errors(I18n.t('errors.authentication.invalid_token'), :forbidden) 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Api 2 | module V1 3 | class UsersController < Api::V1::ApiController 4 | helper_method :user 5 | 6 | before_action :require_check_if_changing_password, only: :update 7 | 8 | def show; end 9 | 10 | def update 11 | user.update!(user_params) 12 | end 13 | 14 | private 15 | 16 | def user 17 | @user ||= current_user 18 | end 19 | 20 | def require_check_if_changing_password 21 | return if !user_params[:password] || user.valid_password?(params[:password_check]) 22 | 23 | render_errors(I18n.t('errors.authentication.invalid_password_check'), :bad_request) 24 | end 25 | 26 | def user_params 27 | params.require(:user).permit(:email, :password, :first_name, :last_name, :locale) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery prepend: true, with: :exception, unless: -> { request.format.json? } 3 | end 4 | -------------------------------------------------------------------------------- /app/controllers/concerns/act_as_api_request.rb: -------------------------------------------------------------------------------- 1 | module ActAsApiRequest 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | before_action :skip_session_storage 6 | before_action :check_request_type 7 | end 8 | 9 | def check_request_type 10 | return if request_body.empty? 11 | 12 | allowed_types = %w[json form-data] 13 | content_type = request.content_type 14 | 15 | return if content_type.match(Regexp.union(allowed_types)) 16 | 17 | render json: { error: I18n.t('errors.invalid_content_type') }, status: :bad_request 18 | end 19 | 20 | def skip_session_storage 21 | request.session_options[:skip] = true 22 | end 23 | 24 | private 25 | 26 | def request_body 27 | request.body.read 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /app/controllers/concerns/exception_handler.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHandler 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | rescue_from StandardError, with: :handle_standard_error 6 | rescue_from ActionController::ParameterMissing, with: :render_parameter_missing 7 | rescue_from ActiveRecord::RecordNotFound, with: :render_record_not_found 8 | rescue_from ActionView::Template::Error, with: :handle_template_error 9 | rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid 10 | rescue_from Pagy::OverflowError, with: :render_page_overflow 11 | 12 | before_action :set_sentry_context 13 | end 14 | 15 | private 16 | 17 | def render_errors(error_messages, status) 18 | error_messages = Array(error_messages) 19 | 20 | render json: { errors: error_messages }, status: status 21 | end 22 | 23 | def render_attributes_errors(error_messages) 24 | render json: { attributes_errors: error_messages }, status: :unprocessable_entity 25 | end 26 | 27 | def render_parameter_missing(exception) 28 | render_errors(I18n.t('errors.missing_param', param: exception.param.to_s), :bad_request) 29 | end 30 | 31 | def render_record_not_found(exception) 32 | render_errors(I18n.t('errors.record_not_found', model: exception.model), :not_found) 33 | end 34 | 35 | def render_record_invalid(exception) 36 | errors = exception.record.errors.messages 37 | 38 | render_attributes_errors(errors) 39 | end 40 | 41 | def render_page_overflow 42 | render_errors(I18n.t('errors.page_overflow'), :unprocessable_entity) 43 | end 44 | 45 | def handle_standard_error(exception) 46 | raise exception if Rails.env.test? 47 | 48 | logger.error(exception) 49 | exception.backtrace.each { |line| logger.error(line) } if Rails.env.development? 50 | 51 | Sentry.capture_exception(exception) 52 | 53 | render_errors(I18n.t('errors.server'), :internal_server_error) 54 | end 55 | 56 | def handle_template_error(exception) 57 | cause = exception.cause 58 | 59 | if cause.is_a?(ActiveRecord::RecordNotFound) 60 | render_record_not_found(cause) 61 | else 62 | handle_standard_error(cause) 63 | end 64 | end 65 | 66 | def set_sentry_context 67 | Sentry.set_extras(params: params.to_unsafe_h, url: request.url) 68 | return if current_user.nil? 69 | 70 | Sentry.set_context('context', { id: current_user.id, email: current_user.email }) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /app/controllers/concerns/localizable.rb: -------------------------------------------------------------------------------- 1 | module Localizable 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | around_action :switch_locale 6 | end 7 | 8 | private 9 | 10 | def switch_locale(&action) 11 | I18n.with_locale(valid_locale, &action) 12 | end 13 | 14 | def valid_locale 15 | locale = current_user&.locale || params[:locale] 16 | I18n.available_locales.include?(locale.try(:to_sym)) ? locale : I18n.default_locale 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | layout 'mailer' 3 | end 4 | -------------------------------------------------------------------------------- /app/models/admin_user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: admin_users 4 | # 5 | # id :bigint not null, primary key 6 | # email :string not null, indexed 7 | # encrypted_password :string not null 8 | # remember_created_at :datetime 9 | # reset_password_sent_at :datetime 10 | # reset_password_token :string indexed 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | # Indexes 15 | # 16 | # index_admin_users_on_email (email) UNIQUE 17 | # index_admin_users_on_reset_password_token (reset_password_token) UNIQUE 18 | # 19 | 20 | class AdminUser < ApplicationRecord 21 | devise :database_authenticatable, :recoverable, :rememberable, :validatable 22 | end 23 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | 4 | nilify_blanks nullables_only: false 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/with_stats_scopes.rb: -------------------------------------------------------------------------------- 1 | module WithStatsScopes 2 | def this_month 3 | where(created_at: Time.current.all_month) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # confirmation_sent_at :datetime 7 | # confirmation_token :string indexed 8 | # confirmed_at :datetime 9 | # current_sign_in_at :datetime 10 | # current_sign_in_ip :string 11 | # email :string not null, indexed 12 | # encrypted_password :string not null 13 | # first_name :string 14 | # last_name :string 15 | # last_sign_in_at :datetime 16 | # last_sign_in_ip :string 17 | # locale :string 18 | # must_change_password :boolean default(FALSE) 19 | # provider :string default("email"), not null, indexed => [uid] 20 | # remember_created_at :datetime 21 | # reset_password_sent_at :datetime 22 | # reset_password_token :string indexed 23 | # sign_in_count :integer default(0) 24 | # tokens :json not null 25 | # uid :string not null, indexed => [provider] 26 | # unconfirmed_email :string 27 | # created_at :datetime not null 28 | # updated_at :datetime not null 29 | # 30 | # Indexes 31 | # 32 | # index_users_on_confirmation_token (confirmation_token) UNIQUE 33 | # index_users_on_email (email) UNIQUE 34 | # index_users_on_reset_password_token (reset_password_token) UNIQUE 35 | # index_users_on_uid_and_provider (uid,provider) UNIQUE 36 | # 37 | 38 | class User < ApplicationRecord 39 | devise :database_authenticatable, :recoverable, 40 | :trackable, :validatable, :registerable 41 | 42 | include DeviseTokenAuth::Concerns::User 43 | serialize :tokens 44 | 45 | validates :locale, 46 | inclusion: { in: I18n.available_locales.map(&:to_s), allow_blank: true }, 47 | if: :locale_changed? 48 | 49 | def send_devise_notification(notification, *args) 50 | devise_mailer.send(notification, self, *args).deliver_later 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/services/application_service.rb: -------------------------------------------------------------------------------- 1 | class ApplicationService 2 | class << self 3 | def call(*args, &block) 4 | service = new(*args, &block) 5 | service.call 6 | service 7 | end 8 | end 9 | 10 | attr_reader :result 11 | 12 | def call; end 13 | 14 | def success? 15 | errors.empty? 16 | end 17 | 18 | def errors 19 | @errors ||= [] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/views/admin/dashboard/_sign_ups_per_month.html.erb: -------------------------------------------------------------------------------- 1 | <%= column_chart users %> 2 | -------------------------------------------------------------------------------- /app/views/api/v1/registrations/create.json.jb: -------------------------------------------------------------------------------- 1 | { 2 | user: render('api/v1/users/minimal', user: @resource) 3 | } 4 | -------------------------------------------------------------------------------- /app/views/api/v1/sessions/create.json.jb: -------------------------------------------------------------------------------- 1 | { 2 | user: render('api/v1/users/minimal', user: @resource), 3 | must_change_password: @resource.must_change_password 4 | } 5 | -------------------------------------------------------------------------------- /app/views/api/v1/token_validations/validate.jb: -------------------------------------------------------------------------------- 1 | { 2 | user: render('api/v1/users/minimal', user: @resource) 3 | } 4 | -------------------------------------------------------------------------------- /app/views/api/v1/users/_minimal.json.jb: -------------------------------------------------------------------------------- 1 | { 2 | id: user.id, 3 | first_name: user.first_name, 4 | last_name: user.last_name, 5 | email: user.email, 6 | locale: user.locale, 7 | created_at: user.created_at, 8 | updated_at: user.updated_at 9 | } 10 | -------------------------------------------------------------------------------- /app/views/api/v1/users/show.json.jb: -------------------------------------------------------------------------------- 1 | { 2 | user: render('api/v1/users/minimal', user: user) 3 | } 4 | -------------------------------------------------------------------------------- /app/views/api/v1/users/update.json.jb: -------------------------------------------------------------------------------- 1 | { 2 | user: render('api/v1/users/minimal', user: @resource) 3 | } 4 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 47 | 48 |
49 |

<%= I18n.t('devise.mailer.reset_password_instructions.action') %>

50 |

<%= I18n.t('devise.mailer.reset_password_instructions.instruction') %>:

51 |

<%= @token %> 52 |

<%= I18n.t('devise.mailer.reset_password_instructions.instruction_3') %>

53 |
<%= ENV.fetch('SITE_TITLE') %>
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.text.erb: -------------------------------------------------------------------------------- 1 | <%= I18n.t('devise.mailer.reset_password_instructions.action') %> 2 | 3 | <%= I18n.t('devise.mailer.reset_password_instructions.instruction') %>: 4 | 5 | <%= @token %> 6 | 7 | <%= I18n.t('devise.mailer.reset_password_instructions.instruction_3') %> 8 | 9 | <%= ENV.fetch('SITE_TITLE') %> 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('spring', __dir__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('spring', __dir__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path('..', __dir__) 6 | 7 | def system!(*args) 8 | system(*args) || abort("\n== Command #{args} failed ==") 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to setup or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at anytime and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?('config/database.yml') 22 | # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! 'bin/rails db:prepare' 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! 'bin/rails log:clear tmp:clear' 30 | 31 | puts "\n== Restarting application server ==" 32 | system! 'bin/rails restart' 33 | end 34 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails' 4 | require 'active_model/railtie' 5 | require 'active_job/railtie' 6 | require 'active_record/railtie' 7 | require 'active_storage/engine' 8 | require 'action_controller/railtie' 9 | require 'action_mailer/railtie' 10 | require 'action_mailbox/engine' 11 | require 'action_text/engine' 12 | require 'action_view/railtie' 13 | require 'action_cable/engine' 14 | require 'rails/test_unit/railtie' 15 | 16 | Bundler.require(*Rails.groups) 17 | 18 | module RailsApiBoilerplate 19 | class Application < Rails::Application 20 | config.load_defaults 6.0 21 | 22 | config.active_job.queue_adapter = :sidekiq 23 | config.time_zone = ENV.fetch('TZ', 'Eastern Time (US & Canada)') 24 | config.active_record.default_timezone = :utc 25 | 26 | ActionMailer::Base.delivery_method = :smtp 27 | ActionMailer::Base.smtp_settings = { 28 | address: 'smtp.sendgrid.net', 29 | port: 25, 30 | domain: ENV.fetch('MAILER_DOMAIN', nil), 31 | authentication: :plain, 32 | user_name: 'apikey', 33 | password: ENV.fetch('SENDGRID_API_KEY', nil), 34 | enable_starttls_auto: true 35 | } 36 | 37 | config.action_mailer.default_url_options = { host: ENV.fetch('SERVER_URL', nil) } 38 | config.action_mailer.default_options = { 39 | from: ENV.fetch('DEFAULT_FROM_EMAIL_ADDRESS', nil), 40 | reply_to: ENV.fetch('DEFAULT_FROM_EMAIL_ADDRESS', nil) 41 | } 42 | 43 | config.generators do |gen| 44 | gen.test_framework :rspec 45 | gen.fixture_replacement :factory_bot, dir: 'spec/factories' 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: rails_api_boilerplate_production 11 | -------------------------------------------------------------------------------- /config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | xy/WyIotsCIpWjn+du1myy4Bjqg7MAU6Vw/Xyylb7rARdHYv9ZUx1XZ2LzJUqCuJtZHlYMFk9hDpOns73KRW35F+hMlO+DPxVMFTbi0Gw0/Jrzn1jnxL7ec0fZTeqxi4cEqNdz5f/0XDSM3NdilsOxCzdEI3MQYrXFn1R7VjjvtQDSN5FsMfmxoYGfGgoRBOPNSEXtT2+3e72EObnGEmFQbAfeGKEif5ufui8Ko0Xn3w4sxtJ/Xy+MJAX3VSQ65SCsxD/OOGFFE6jlOHHW4UcVatH+2if64Z2OkQfBH8DxyPGR6xrm6XdMcaUHWdh0ymYyzKI44CQqp5WDPM9KnaYmh7KF5nKsqlBFXqdTEsTMu1lC+J3xStg7Y2x3kmfCZA98wipCfx7QzCO+iKnv+gFvkg9G5XCjCmjxwJ--OIPtzCoXE8V2CmxK--O3ErDrOsV+koU7aW87RJfg== -------------------------------------------------------------------------------- /config/credentials/development.yml.enc: -------------------------------------------------------------------------------- 1 | 7pSM0M1PGil3vf0Q2Kx8EImrqAf1DaueXy4IimnJB8SJ8P3w/z6PWVJLE1jnu3dw2Dpz3ybXwHksbH4BhDsdfxIY78+cIhjtml++lAM2g+h31jV4NVFnxiR/oJ2WJFRIzsEkAcdsy0NYz1KOR2WKyDZ5+sCpTcGUP0K3oSaG8IeO7sWPtTOL+eS2nd5I2+Z2UF10ulRuj44gk8MK2E0SZ8Da2yzYXqOcnEtSTCRHWddVNw1oSSnh8K1sFWqS/cpVUTXM2fykCpkDHQ0sFu76S1oEXnEFkpzD--x4WS2G0zT2ZrLZAS--TyI8AtZAWmgZVROyJUDHKg== -------------------------------------------------------------------------------- /config/credentials/test.yml.enc: -------------------------------------------------------------------------------- 1 | luuplRPk4/WRkQyMZvk6ztJ2nQnp4LwzIbSJVJFih5EMwcCnxzHQBcvOkpTZp2hkOOZyauCZ8csUeB6osQeuQImsyCpBA0lmw7RIJkrn6jxNf0iApP4nh+iihRKn45NQLqZ5DRdMwL9TxyTFsr2tnEWmjdLoVNHO8TpoUNhITXZJ2GOfPjGwANzBGoQg70IkQ8iDf9MEOA==--1rCo/bO9W66t4Hyk--y2FjSD2p6Xy6xXoG67SMqg== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | host: <%= ENV['DB_HOST'] %> 5 | pool: 5 6 | timeout: 5000 7 | username: <%= ENV['DB_USERNAME'] %> 8 | password: <%= ENV['DB_PASSWORD'] %> 9 | database: <%= ENV['DB_NAME'] %> 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | database: <%= "#{ENV['DB_NAME']}-test" %> 17 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp/caching-dev.txt').exist? 18 | config.cache_store = :memory_store 19 | config.public_file_server.headers = { 20 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 21 | } 22 | else 23 | config.action_controller.perform_caching = false 24 | 25 | config.cache_store = :null_store 26 | end 27 | 28 | # Store uploaded files on the local file system (see config/storage.yml for options). 29 | config.active_storage.service = :local 30 | 31 | # Mailer 32 | config.action_mailer.default_url_options = { host: 'localhost' } 33 | config.action_mailer.delivery_method = :letter_opener 34 | config.action_mailer.perform_deliveries = true 35 | config.action_mailer.raise_delivery_errors = false 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Highlight code that triggered database queries in logs. 45 | config.active_record.verbose_query_logs = true 46 | 47 | # Raises error for missing translations. 48 | # config.action_view.raise_on_missing_translations = true 49 | 50 | # Use an evented file watcher to asynchronously detect changes in source code, 51 | # routes, locales, etc. This feature depends on the listen gem. 52 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 53 | 54 | config.after_initialize do 55 | Bullet.enable = true 56 | Bullet.alert = false 57 | Bullet.bullet_logger = true 58 | Bullet.console = true 59 | Bullet.rails_logger = true 60 | Bullet.add_footer = true 61 | end 62 | 63 | config.action_mailer.preview_path = Rails.root.join('spec/mailers/previews') 64 | config.cache_store = :memory_store 65 | end 66 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 26 | # config.action_controller.asset_host = 'http://assets.example.com' 27 | 28 | # Specifies the header that your server uses for sending files. 29 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 30 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 31 | 32 | # Store uploaded files on the local file system (see config/storage.yml for options). 33 | # config.active_storage.service = :local 34 | 35 | # Mount Action Cable outside main process or domain. 36 | # config.action_cable.mount_path = nil 37 | # config.action_cable.url = 'wss://example.com/cable' 38 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 39 | 40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 41 | # config.force_ssl = true 42 | 43 | # Use the lowest log level to ensure availability of diagnostic information 44 | # when problems arise. 45 | config.log_level = :debug 46 | 47 | # Prepend all log lines with the following tags. 48 | config.log_tags = [:request_id] 49 | 50 | # Use a different cache store in production. 51 | # config.cache_store = :mem_cache_store 52 | 53 | # Use a real queuing backend for Active Job (and separate queues per environment). 54 | # config.active_job.queue_adapter = :resque 55 | # config.active_job.queue_name_prefix = "rails_api_boilerplate_production" 56 | 57 | config.action_mailer.perform_caching = false 58 | 59 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 60 | # the I18n.default_locale when a translation cannot be found). 61 | config.i18n.fallbacks = true 62 | 63 | # Send deprecation notices to registered listeners. 64 | config.active_support.deprecation = :notify 65 | 66 | # Use default logging formatter so that PID and timestamp are not suppressed. 67 | config.log_formatter = ::Logger::Formatter.new 68 | 69 | # Use a different logger for distributed setups. 70 | # require 'syslog/logger' 71 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 72 | 73 | if ENV['RAILS_LOG_TO_STDOUT'].present? 74 | logger = ActiveSupport::Logger.new($stdout) 75 | logger.formatter = config.log_formatter 76 | config.logger = ActiveSupport::TaggedLogging.new(logger) 77 | end 78 | 79 | # Do not dump schema after migrations. 80 | config.active_record.dump_schema_after_migration = false 81 | end 82 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. This avoids loading your whole application 12 | # just for the purpose of running a single test. If you are using a tool that 13 | # preloads Rails for running tests, you may have to set it to true. 14 | config.eager_load = false 15 | 16 | # Configure public file server for tests with Cache-Control for performance. 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 19 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 20 | } 21 | 22 | # Show full error reports and disable caching. 23 | config.consider_all_requests_local = true 24 | config.action_controller.perform_caching = false 25 | config.cache_store = :null_store 26 | 27 | # Raise exceptions instead of rendering exception templates. 28 | config.action_dispatch.show_exceptions = false 29 | 30 | # Disable request forgery protection in test environment. 31 | config.action_controller.allow_forgery_protection = false 32 | 33 | # Store uploaded files on the local file system in a temporary directory. 34 | config.active_storage.service = :test 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Tell Action Mailer not to deliver emails to the real world. 39 | # The :test delivery method accumulates sent emails in the 40 | # ActionMailer::Base.deliveries array. 41 | config.action_mailer.delivery_method = :test 42 | config.action_mailer.perform_deliveries = true 43 | config.action_mailer.raise_delivery_errors = true 44 | config.action_mailer.default_url_options = { host: 'localhost' } 45 | 46 | # Print deprecation notices to the stderr. 47 | config.active_support.deprecation = :stderr 48 | 49 | # Raises error for missing translations. 50 | config.i18n.raise_on_missing_translations = true 51 | 52 | config.after_initialize do 53 | Bullet.enable = true 54 | Bullet.bullet_logger = false 55 | Bullet.rails_logger = true 56 | Bullet.n_plus_one_query_enable = true 57 | Bullet.unused_eager_loading_enable = true 58 | Bullet.counter_cache_enable = true 59 | Bullet.raise = true 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /config/initializers/active_admin.rb: -------------------------------------------------------------------------------- 1 | ActiveAdmin.setup do |config| 2 | config.site_title = ENV.fetch('SITE_TITLE', 'Admin') 3 | config.favicon = '/assets/favicon.png' 4 | config.authentication_method = :authenticate_admin_user! 5 | config.current_user_method = :current_admin_user 6 | config.logout_link_path = :destroy_admin_user_session_path 7 | config.batch_actions = true 8 | config.filter_attributes = %i[encrypted_password password password_confirmation] 9 | config.localize_format = :long 10 | config.comments = false 11 | end 12 | -------------------------------------------------------------------------------- /config/initializers/activeadmin_addons.rb: -------------------------------------------------------------------------------- 1 | ActiveadminAddons.setup do |config| 2 | config.default_select = 'select2' 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | allow do 10 | origins '*' 11 | 12 | resource '*', 13 | headers: :any, 14 | expose: %w[access-token expiry token-type uid 15 | client Link Current-Page Page-Items 16 | Total-Pages Total-Count], 17 | methods: %i[get post put patch delete options head] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../../lib/gem_extensions/devise/token_generator' 4 | # Use this hook to configure devise mailer, warden hooks and so forth. 5 | # Many of these configuration options can be set straight in your model. 6 | Devise.setup do |config| 7 | # The secret key used by Devise. Devise uses this key to generate 8 | # random tokens. Changing this key will render invalid all existing 9 | # confirmation, reset password and unlock tokens in the database. 10 | # Devise will use the `secret_key_base` as its `secret_key` 11 | # by default. You can change it below and use your own secret key. 12 | # config.secret_key = '8f2651d7b0b8b72f31998169e528223dff2628274eefaa51da9f105d34d98a1dc44735c54a3cae0e28b2a5a9b5631806d50b1ac7f33fdb2375eec87732cb1bdd' 13 | 14 | # ==> Controller configuration 15 | # Configure the parent class to the devise controllers. 16 | # config.parent_controller = 'DeviseController' 17 | 18 | # ==> Mailer Configuration 19 | # Configure the e-mail address which will be shown in Devise::Mailer, 20 | # note that it will be overwritten if you use your own mailer class 21 | # with default "from" parameter. 22 | config.mailer_sender = ENV.fetch('DEFAULT_FROM_EMAIL_ADDRESS') 23 | 24 | # Configure the class responsible to send e-mails. 25 | # config.mailer = 'Devise::Mailer' 26 | 27 | # Configure the parent class responsible to send e-mails. 28 | # config.parent_mailer = 'ActionMailer::Base' 29 | 30 | # ==> ORM configuration 31 | # Load and configure the ORM. Supports :active_record (default) and 32 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 33 | # available as additional gems. 34 | require 'devise/orm/active_record' 35 | 36 | # ==> Configuration for any authentication mechanism 37 | # Configure which keys are used when authenticating a user. The default is 38 | # just :email. You can configure it to use [:username, :subdomain], so for 39 | # authenticating a user, both parameters are required. Remember that those 40 | # parameters are used only when authenticating and not when retrieving from 41 | # session. If you need permissions, you should implement that in a before filter. 42 | # You can also supply a hash where the value is a boolean determining whether 43 | # or not authentication should be aborted when the value is not present. 44 | # config.authentication_keys = [:email] 45 | 46 | # Configure parameters from the request object used for authentication. Each entry 47 | # given should be a request method and it will automatically be passed to the 48 | # find_for_authentication method and considered in your model lookup. For instance, 49 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 50 | # The same considerations mentioned for authentication_keys also apply to request_keys. 51 | # config.request_keys = [] 52 | 53 | # Configure which authentication keys should be case-insensitive. 54 | # These keys will be downcased upon creating or modifying a user and when used 55 | # to authenticate or find a user. Default is :email. 56 | config.case_insensitive_keys = [:email] 57 | 58 | # Configure which authentication keys should have whitespace stripped. 59 | # These keys will have whitespace before and after removed upon creating or 60 | # modifying a user and when used to authenticate or find a user. Default is :email. 61 | config.strip_whitespace_keys = [:email] 62 | 63 | # Tell if authentication through request.params is enabled. True by default. 64 | # It can be set to an array that will enable params authentication only for the 65 | # given strategies, for example, `config.params_authenticatable = [:database]` will 66 | # enable it only for database (email + password) authentication. 67 | # config.params_authenticatable = true 68 | 69 | # Tell if authentication through HTTP Auth is enabled. False by default. 70 | # It can be set to an array that will enable http authentication only for the 71 | # given strategies, for example, `config.http_authenticatable = [:database]` will 72 | # enable it only for database authentication. The supported strategies are: 73 | # :database = Support basic authentication with authentication key + password 74 | # config.http_authenticatable = false 75 | 76 | # If 401 status code should be returned for AJAX requests. True by default. 77 | # config.http_authenticatable_on_xhr = true 78 | 79 | # The realm used in Http Basic Authentication. 'Application' by default. 80 | # config.http_authentication_realm = 'Application' 81 | 82 | # It will change confirmation, password recovery and other workflows 83 | # to behave the same regardless if the e-mail provided was right or wrong. 84 | # Does not affect registerable. 85 | # config.paranoid = true 86 | 87 | # By default Devise will store the user in session. You can skip storage for 88 | # particular strategies by setting this option. 89 | # Notice that if you are skipping storage for all authentication paths, you 90 | # may want to disable generating routes to Devise's sessions controller by 91 | # passing skip: :sessions to `devise_for` in your config/routes.rb 92 | config.skip_session_storage = [:http_auth] 93 | 94 | # By default, Devise cleans up the CSRF token on authentication to 95 | # avoid CSRF token fixation attacks. This means that, when using AJAX 96 | # requests for sign in and sign up, you need to get a new CSRF token 97 | # from the server. You can disable this option at your own risk. 98 | # config.clean_up_csrf_token_on_authentication = true 99 | 100 | # When false, Devise will not attempt to reload routes on eager load. 101 | # This can reduce the time taken to boot the app but if your application 102 | # requires the Devise mappings to be loaded during boot time the application 103 | # won't boot properly. 104 | # config.reload_routes = true 105 | 106 | # ==> Configuration for :database_authenticatable 107 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 108 | # using other algorithms, it sets how many times you want the password to be hashed. 109 | # 110 | # Limiting the stretches to just one in testing will increase the performance of 111 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 112 | # a value less than 10 in other environments. Note that, for bcrypt (the default 113 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 114 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 115 | config.stretches = Rails.env.test? ? 1 : 11 116 | 117 | # Set up a pepper to generate the hashed password. 118 | # config.pepper = '164e8f2ca8f1882a98e61dfe69623813d9140ccf6b5a5e8f55dc82a1b0d81851678ef5e5734bf0c6aa37fa8f3a70bdd3b4c26a30c4c4ff00d49dc283663edbd6' 119 | 120 | # Send a notification to the original email when the user's email is changed. 121 | # config.send_email_changed_notification = false 122 | 123 | # Send a notification email when the user's password is changed. 124 | # config.send_password_change_notification = false 125 | 126 | # ==> Configuration for :confirmable 127 | # A period that the user is allowed to access the website even without 128 | # confirming their account. For instance, if set to 2.days, the user will be 129 | # able to access the website for two days without confirming their account, 130 | # access will be blocked just in the third day. Default is 0.days, meaning 131 | # the user cannot access the website without confirming their account. 132 | # config.allow_unconfirmed_access_for = 2.days 133 | 134 | # A period that the user is allowed to confirm their account before their 135 | # token becomes invalid. For example, if set to 3.days, the user can confirm 136 | # their account within 3 days after the mail was sent, but on the fourth day 137 | # their account can't be confirmed with the token any more. 138 | # Default is nil, meaning there is no restriction on how long a user can take 139 | # before confirming their account. 140 | # config.confirm_within = 3.days 141 | 142 | # If true, requires any email changes to be confirmed (exactly the same way as 143 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 144 | # db field (see migrations). Until confirmed, new email is stored in 145 | # unconfirmed_email column, and copied to email column on successful confirmation. 146 | config.reconfirmable = true 147 | 148 | # Defines which key will be used when confirming an account 149 | # config.confirmation_keys = [:email] 150 | 151 | # ==> Configuration for :rememberable 152 | # The time the user will be remembered without asking for credentials again. 153 | # config.remember_for = 2.weeks 154 | 155 | # Invalidates all the remember me tokens when the user signs out. 156 | config.expire_all_remember_me_on_sign_out = true 157 | 158 | # If true, extends the user's remember period when remembered via cookie. 159 | # config.extend_remember_period = false 160 | 161 | # Options to be passed to the created cookie. For instance, you can set 162 | # secure: true in order to force SSL only cookies. 163 | # config.rememberable_options = {} 164 | 165 | # ==> Configuration for :validatable 166 | # Range for password length. 167 | config.password_length = 6..128 168 | 169 | # Email regex used to validate email formats. It simply asserts that 170 | # one (and only one) @ exists in the given string. This is mainly 171 | # to give user feedback and not to assert the e-mail validity. 172 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 173 | 174 | # ==> Configuration for :timeoutable 175 | # The time you want to timeout the user session without activity. After this 176 | # time the user will be asked for credentials again. Default is 30 minutes. 177 | # config.timeout_in = 30.minutes 178 | 179 | # ==> Configuration for :lockable 180 | # Defines which strategy will be used to lock an account. 181 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 182 | # :none = No lock strategy. You should handle locking by yourself. 183 | # config.lock_strategy = :failed_attempts 184 | 185 | # Defines which key will be used when locking and unlocking an account 186 | # config.unlock_keys = [:email] 187 | 188 | # Defines which strategy will be used to unlock an account. 189 | # :email = Sends an unlock link to the user email 190 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 191 | # :both = Enables both strategies 192 | # :none = No unlock strategy. You should handle unlocking by yourself. 193 | # config.unlock_strategy = :both 194 | 195 | # Number of authentication tries before locking an account if lock_strategy 196 | # is failed attempts. 197 | # config.maximum_attempts = 20 198 | 199 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 200 | # config.unlock_in = 1.hour 201 | 202 | # Warn on the last attempt before the account is locked. 203 | # config.last_attempt_warning = true 204 | 205 | # ==> Configuration for :recoverable 206 | # 207 | # Defines which key will be used when recovering the password for an account 208 | # config.reset_password_keys = [:email] 209 | 210 | # Time interval you can reset your password with a reset password key. 211 | # Don't put a too small interval or your users won't have the time to 212 | # change their passwords. 213 | config.reset_password_within = 6.hours 214 | 215 | # When set to false, does not sign a user in automatically after their password is 216 | # reset. Defaults to true, so a user is signed in automatically after a reset. 217 | # config.sign_in_after_reset_password = true 218 | 219 | # ==> Configuration for :encryptable 220 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 221 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 222 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 223 | # for default behavior) and :restful_authentication_sha1 (then you should set 224 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 225 | # 226 | # Require the `devise-encryptable` gem when using anything other than bcrypt 227 | # config.encryptor = :sha512 228 | 229 | # ==> Scopes configuration 230 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 231 | # "users/sessions/new". It's turned off by default because it's slower if you 232 | # are using only default views. 233 | # config.scoped_views = false 234 | 235 | # Configure the default scope given to Warden. By default it's the first 236 | # devise role declared in your routes (usually :user). 237 | # config.default_scope = :user 238 | 239 | # Set this configuration to false if you want /users/sign_out to sign out 240 | # only the current scope. By default, Devise signs out all scopes. 241 | # config.sign_out_all_scopes = true 242 | 243 | # ==> Navigation configuration 244 | # Lists the formats that should be treated as navigational. Formats like 245 | # :html, should redirect to the sign in page when the user does not have 246 | # access, but formats like :xml or :json, should return 401. 247 | # 248 | # If you have any extra navigational formats, like :iphone or :mobile, you 249 | # should add them to the navigational formats lists. 250 | # 251 | # The "*/*" below is required to match Internet Explorer requests. 252 | # config.navigational_formats = ['*/*', :html] 253 | 254 | # The default HTTP method used to sign out a resource. Default is :delete. 255 | config.sign_out_via = :delete 256 | 257 | # ==> OmniAuth 258 | # Add a new OmniAuth provider. Check the wiki for more information on setting 259 | # up on your models and hooks. 260 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 261 | 262 | # ==> Warden configuration 263 | # If you want to use other strategies, that are not supported by Devise, or 264 | # change the failure app, you can configure them inside the config.warden block. 265 | # 266 | # config.warden do |manager| 267 | # manager.intercept_401 = false 268 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 269 | # end 270 | 271 | # ==> Mountable engine configurations 272 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 273 | # is mountable, there are some extra configurations to be taken into account. 274 | # The following options are available, assuming the engine is mounted as: 275 | # 276 | # mount MyEngine, at: '/my_engine' 277 | # 278 | # The router that invoked `devise_for`, in the example above, would be: 279 | # config.router_name = :my_engine 280 | # 281 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 282 | # so you need to do it manually. For the users scope, it would be: 283 | # config.omniauth_path_prefix = '/my_engine/users/auth' 284 | 285 | # ==> Turbolinks configuration 286 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: 287 | # 288 | # ActiveSupport.on_load(:devise_failure_app) do 289 | # include Turbolinks::Controller 290 | # end 291 | end 292 | 293 | Devise::TokenGenerator.prepend(::GemExtensions::Devise::TokenGenerator) 294 | -------------------------------------------------------------------------------- /config/initializers/devise_token_auth.rb: -------------------------------------------------------------------------------- 1 | DeviseTokenAuth.setup do |config| 2 | config.change_headers_on_each_request = false 3 | config.remove_tokens_after_password_reset = true 4 | config.token_lifespan = 20.weeks 5 | config.max_number_of_devices = 5 6 | config.batch_request_buffer_throttle = 10.seconds 7 | config.token_cost = Rails.env.test? ? 1 : 8 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += %i[password password_confirmation] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/locale.rb: -------------------------------------------------------------------------------- 1 | I18n.available_locales = %i[en es] 2 | I18n.default_locale = :en 3 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/pagy.rb: -------------------------------------------------------------------------------- 1 | require 'pagy/extras/headers' 2 | -------------------------------------------------------------------------------- /config/initializers/rack_attack.rb: -------------------------------------------------------------------------------- 1 | module Rack 2 | class Attack 3 | ### Configure Cache ### 4 | 5 | # If you don't want to use Rails.cache (Rack::Attack's default), then 6 | # configure it here. 7 | # 8 | # Note: The store is only used for throttling (not blocklisting and 9 | # safelisting). It must implement .increment and .write like 10 | # ActiveSupport::Cache::Store 11 | 12 | # Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new 13 | 14 | ### Throttle Spammy Clients ### 15 | 16 | # If any single client IP is making tons of requests, then they're 17 | # probably malicious or a poorly-configured scraper. Either way, they 18 | # don't deserve to hog all of the app server's CPU. Cut them off! 19 | # 20 | # Note: If you're serving assets through rack, those requests may be 21 | # counted by rack-attack and this throttle may be activated too 22 | # quickly. If so, enable the condition to exclude them from tracking. 23 | 24 | # Throttle all requests by IP (60rpm) 25 | # 26 | # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" 27 | throttle('req/ip', limit: 300, period: 5.minutes, &:ip) 28 | 29 | ### Prevent Brute-Force Login Attacks ### 30 | 31 | # The most common brute-force login attack is a brute-force password 32 | # attack where an attacker simply tries a large number of emails and 33 | # passwords to see if any credentials match. 34 | # 35 | # Another common method of attack is to use a swarm of computers with 36 | # different IPs to try brute-forcing a password for a specific account. 37 | 38 | # Throttle POST requests to /login by IP address 39 | # 40 | # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" 41 | throttle('logins/ip', limit: 5, period: 20.seconds) do |req| 42 | req.ip if req.path == '/api/v1/users/sign_in' && req.post? 43 | end 44 | 45 | # Throttle POST requests to /login by email param 46 | # 47 | # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{normalized_email}" 48 | # 49 | # Note: This creates a problem where a malicious user could intentionally 50 | # throttle logins for another user and force their login requests to be 51 | # denied, but that's not very common and shouldn't happen to you. (Knock 52 | # on wood!) 53 | throttle('logins/email', limit: 5, period: 20.seconds) do |req| 54 | if req.path == '/api/v1/users/sign_in' && req.post? && req.params['user'] 55 | # Normalize the email, using the same logic as your authentication process, to 56 | # protect against rate limit bypasses. Return the normalized email if present, nil otherwise. 57 | req.params['user'].try(:[], 'email')&.downcase&.strip.presence 58 | end 59 | end 60 | 61 | ### Custom Throttle Response ### 62 | 63 | # By default, Rack::Attack returns an HTTP 429 for throttled responses, 64 | # which is just fine. 65 | # 66 | # If you want to return 503 so that the attacker might be fooled into 67 | # believing that they've successfully broken your app (or you just want to 68 | # customize the response), then uncomment these lines. 69 | self.throttled_responder = lambda do |_env| 70 | [ 71 | 429, 72 | { 'Content-Type' => 'application/json' }, 73 | [{ errors: [I18n.t('errors.network_throttling')] }.to_json] 74 | ] 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /config/initializers/sentry.rb: -------------------------------------------------------------------------------- 1 | Sentry.init do |config| 2 | config.dsn = ENV.fetch('SENTRY_DSN', nil) 3 | config.breadcrumbs_logger = %i[active_support_logger http_logger] 4 | config.send_default_pii = true 5 | config.sample_rate = 1.0 6 | config.async = ->(event) { Sentry::SendEventJob.perform_later(event) } 7 | 8 | # Param filtering 9 | filter = ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters) 10 | config.before_send = lambda do |event, _hint| 11 | filter.filter(event.to_hash) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | if Rails.env.production? 4 | Sidekiq::Web.use Rack::Auth::Basic do |username, password| 5 | ActiveSupport::SecurityUtils.secure_compare( 6 | ::Digest::SHA256.hexdigest(username), 7 | ::Digest::SHA256.hexdigest(ENV.fetch('JOB_MONITOR_USERNAME', 'admin')) 8 | ) & ActiveSupport::SecurityUtils.secure_compare( 9 | ::Digest::SHA256.hexdigest(password), 10 | ::Digest::SHA256.hexdigest(ENV.fetch('JOB_MONITOR_PASSWORD', 'admin')) 11 | ) 12 | end 13 | end 14 | 15 | Sidekiq.configure_server do |config| 16 | config.redis = { url: ENV.fetch('REDIS_URL', nil) } 17 | end 18 | 19 | Sidekiq.configure_client do |config| 20 | config.redis = { url: ENV.fetch('REDIS_URL', nil) } 21 | end 22 | -------------------------------------------------------------------------------- /config/initializers/strong_migrations.rb: -------------------------------------------------------------------------------- 1 | # Mark existing migrations as safe 2 | StrongMigrations.start_after = 20201104163939 3 | 4 | # Set timeouts for migrations 5 | # If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user 6 | StrongMigrations.lock_timeout = 10.seconds 7 | StrongMigrations.statement_timeout = 1.hour 8 | 9 | # Analyze tables after indexes are added 10 | # Outdated statistics can sometimes hurt performance 11 | StrongMigrations.auto_analyze = true 12 | 13 | # Set the version of the production database 14 | # so the right checks are run in development 15 | # StrongMigrations.target_version = 10 16 | 17 | # Add custom checks 18 | # StrongMigrations.add_check do |method, args| 19 | # if method == :add_index && args[0].to_s == "users" 20 | # stop! "No more indexes on the users table" 21 | # end 22 | # end 23 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | 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." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | action: Recover Password 24 | instruction: Find below the code to change your password 25 | instruction_2: If you did not request this email please ignore it 26 | instruction_3: Thanks! 27 | subject: "Reset password instructions" 28 | unlock_instructions: 29 | subject: "Unlock instructions" 30 | email_changed: 31 | subject: "Email Changed" 32 | password_change: 33 | subject: "Password Changed" 34 | omniauth_callbacks: 35 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 36 | success: "Successfully authenticated from %{kind} account." 37 | passwords: 38 | 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." 39 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 40 | 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." 41 | updated: "Your password has been changed successfully. You are now signed in." 42 | updated_not_active: "Your password has been changed successfully." 43 | registrations: 44 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 45 | signed_up: "Welcome! You have signed up successfully." 46 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 47 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 48 | 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." 49 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 50 | updated: "Your account has been updated successfully." 51 | sessions: 52 | signed_in: "Signed in successfully." 53 | signed_out: "Signed out successfully." 54 | already_signed_out: "Signed out successfully." 55 | unlocks: 56 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 57 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 58 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 59 | errors: 60 | messages: 61 | already_confirmed: "was already confirmed, please try signing in" 62 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 63 | expired: "has expired, please request a new one" 64 | not_found: "not found" 65 | not_locked: "was not locked" 66 | not_saved: 67 | one: "1 error prohibited this %{resource} from being saved:" 68 | other: "%{count} errors prohibited this %{resource} from being saved:" 69 | -------------------------------------------------------------------------------- /config/locales/devise.es.yml: -------------------------------------------------------------------------------- 1 | # Traducciones adicionales en https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | es: 4 | devise: 5 | confirmations: 6 | confirmed: "Tu cuenta ya ha sido confirmada." 7 | confirmed_and_signed_in: "Tu cuenta ya ha sido confirmada. Has sido identificado." 8 | send_instructions: "Recibirás un correo electrónico en unos minutos con instrucciones sobre cómo restablecer tu contraseña." 9 | send_paranoid_instructions: "Si tu correo electrónico existe en nuestra base de datos recibirás un correo electrónico en unos minutos con instrucciones sobre cómo reiniciar tu contraseña." 10 | failure: 11 | already_authenticated: "Ya estás identificado." 12 | inactive: "Tu cuenta aún no ha sido activada." 13 | invalid: "Correo o contraseña inválidos." 14 | invalid_token: "Cadena de autenticación invalida." 15 | locked: "Tu cuenta está bloqueada." 16 | not_found_in_database: "Correo o contraseña inválidos." 17 | timeout: "Tu sesión ha expirado, por favor identifícate de nuevo para continuar." 18 | unauthenticated: "Necesitas iniciar sesión o registrarte para continuar." 19 | unconfirmed: "Debes confirmar tu cuenta para continuar." 20 | last_attempt: "Tienes un intento más antes que tu cuenta quede bloqueada." 21 | mailer: 22 | confirmation_instructions: 23 | subject: "Instrucciones de confirmación" 24 | reset_password_instructions: 25 | subject: "Instrucciones para restablecer tu contraseña" 26 | unlock_instructions: 27 | subject: "Instrucciones de desbloqueo" 28 | omniauth_callbacks: 29 | failure: "No se te pudo autorizar de %{kind} debido a \"%{reason}\"." 30 | success: "Identificado correctamente de %{kind}." 31 | passwords: 32 | no_token: "No puedes acceder a esta página sino es por medio de un enlace para restablecer contraseña. Si accediste desde el enlace para restablecer la contraseña, asegúrate de que la URL esté completa." 33 | send_instructions: "Recibirás un correo electrónico con instrucciones sobre cómo restablecer tu contraseña en unos minutos." 34 | send_paranoid_instructions: "Si tu correo electrónico existe en nuestra base de datos, recibirás un enlace para restablecer la contraseña en unos minutos." 35 | updated: "Tu contraseña se cambió correctamente. Has sido identificado." 36 | updated_not_active: "Tu contraseña se cambió correctamente." 37 | registrations: 38 | destroyed: "¡Adiós! Tu cuenta ha sido cancelada. Esperamos volver a verte pronto." 39 | signed_up: "¡Bienvenido! Has sido identificado." 40 | signed_up_but_inactive: "Te has registrado correctamente, pero no has podido iniciar sesión porque no has activado tu cuenta." 41 | signed_up_but_locked: "Te has registrado correctamente, pero no has podido iniciar sesión porque tu cuenta está bloqueada." 42 | signed_up_but_unconfirmed: "Se te ha enviado un mensaje con un enlace de confirmación. Por favor visita el enlace para activar tu cuenta." 43 | update_needs_confirmation: "Actualizaste tu cuenta correctamente, sin embargo necesitamos verificar tu nueva cuenta de correo. Por favor revisa tu correo electrónico y visita el enlace para finalizar la confirmación de tu nueva dirección de correo electrónico." 44 | updated: "Has actualizado tu cuenta correctamente." 45 | sessions: 46 | signed_in: "Has iniciado sesión satisfactoriamente." 47 | signed_out: "Has cerrado la sesión satisfactoriamente." 48 | unlocks: 49 | send_instructions: "Recibirás un correo electrónico en unos minutos con instrucciones sobre cómo desbloquear tu cuenta." 50 | send_paranoid_instructions: "Si tu cuenta existe, recibirás un correo electrónico en unos minutos con instrucciones sobre cómo desbloquear tu cuenta." 51 | unlocked: "Tu cuenta ha sido desbloqueada. Por favor inicia sesión para continuar." 52 | errors: 53 | messages: 54 | already_confirmed: "ya ha sido confirmado, por favor intenta iniciar sesión." 55 | confirmation_period_expired: "necesita ser confirmado en %{period}, por favor vuelve a solicitarla." 56 | expired: "ha expirado, por favor vuelve a solicitarla." 57 | not_found: "no se encontró." 58 | not_locked: "no estaba bloqueado." 59 | not_saved: 60 | one: "1 error impidió que este %{resource} fuese guardado:" 61 | other: "%{count} errores impidieron que este %{resource} fuese guardado:" 62 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | active_admin: 3 | charts: 4 | sign_ups_per_month: 'Sign ups per month' 5 | general: General 6 | overview: Overview 7 | session_info: Sign in info 8 | this_month: This month 9 | total: Total 10 | dashboard_welcome: 11 | welcome: Welcome to your Admin Backoffice 12 | call_to_action: Here are some stats! 13 | activerecord: 14 | models: 15 | user: 16 | one: User 17 | other: Users 18 | errors: 19 | server: 'An error ocurred' 20 | network_throttling: 'Throttle limit reached' 21 | record_not_found: '%{model} not found' 22 | missing_param: 'The following param is missing or the value is empty: %{param}' 23 | invalid_content_type: 'Invalid content type header' 24 | page_overflow: 'The requested page does not exist' 25 | invalid_reset_password_token: 'the reset password token is invalid' 26 | authentication: 27 | invalid_credentials: 'The credentials are not valid' 28 | invalid_token: 'The token is not valid' 29 | invalid_password_check: 'The current password is not valid' 30 | emails: 31 | reset_password: 32 | recover_account: 'Recover account' 33 | devise: 34 | failure: 35 | unauthenticated: 'Authentication is required to perform this action' 36 | -------------------------------------------------------------------------------- /config/locales/es.yml: -------------------------------------------------------------------------------- 1 | es: 2 | active_admin: 3 | charts: 4 | sign_ups_per_month: 'Registros por mes' 5 | general: General 6 | overview: Resumen 7 | session_info: Información de inicio de sesión 8 | this_month: Este mes 9 | total: Total 10 | dashboard_welcome: 11 | welcome: Bienvenido al Área de Administradores 12 | call_to_action: ¡Ve algunas estadísticas! 13 | activerecord: 14 | models: 15 | user: 16 | one: Usuario 17 | other: Usuarios 18 | errors: 19 | server: 'Ha ocurrido un error' 20 | network_throttling: 'Ha alcanzado el límite de solicitudes' 21 | record_not_found: '%{model} no encontrado' 22 | missing_param: 'Falta el siguiente parámetro o el valor está vacío: %{param}' 23 | invalid_content_type: 'Content-Type inválido en el encabezado' 24 | page_overflow: 'La página solicitada no existe' 25 | invalid_reset_password_token: 'el token para restablecer la contraseña no es válido' 26 | authentication: 27 | invalid_credentials: 'Las credenciales no son válidas' 28 | invalid_token: 'El token no es válido' 29 | invalid_password_check: 'La contraseña actual no es válida' 30 | emails: 31 | reset_password: 32 | recover_account: 'Recuperar cuenta' 33 | devise: 34 | failure: 35 | unauthenticated: 'Se requiere autenticación para realizar esta acción' 36 | -------------------------------------------------------------------------------- /config/locales/rails.es.yml: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/es.yml 2 | es: 3 | activerecord: 4 | errors: 5 | messages: 6 | record_invalid: 'La validación falló: %{errors}' 7 | restrict_dependent_destroy: 8 | has_one: No se puede eliminar el registro porque existe un %{record} dependiente 9 | has_many: No se puede eliminar el registro porque existen %{record} dependientes 10 | date: 11 | abbr_day_names: 12 | - dom 13 | - lun 14 | - mar 15 | - mié 16 | - jue 17 | - vie 18 | - sáb 19 | abbr_month_names: 20 | - 21 | - ene 22 | - feb 23 | - mar 24 | - abr 25 | - may 26 | - jun 27 | - jul 28 | - ago 29 | - sep 30 | - oct 31 | - nov 32 | - dic 33 | day_names: 34 | - domingo 35 | - lunes 36 | - martes 37 | - miércoles 38 | - jueves 39 | - viernes 40 | - sábado 41 | formats: 42 | default: "%-d/%-m/%Y" 43 | long: "%-d de %B de %Y" 44 | short: "%-d de %b" 45 | month_names: 46 | - 47 | - enero 48 | - febrero 49 | - marzo 50 | - abril 51 | - mayo 52 | - junio 53 | - julio 54 | - agosto 55 | - septiembre 56 | - octubre 57 | - noviembre 58 | - diciembre 59 | order: 60 | - :day 61 | - :month 62 | - :year 63 | datetime: 64 | distance_in_words: 65 | about_x_hours: 66 | one: alrededor de 1 hora 67 | other: alrededor de %{count} horas 68 | about_x_months: 69 | one: alrededor de 1 mes 70 | other: alrededor de %{count} meses 71 | about_x_years: 72 | one: alrededor de 1 año 73 | other: alrededor de %{count} años 74 | almost_x_years: 75 | one: casi 1 año 76 | other: casi %{count} años 77 | half_a_minute: medio minuto 78 | less_than_x_seconds: 79 | one: menos de 1 segundo 80 | other: menos de %{count} segundos 81 | less_than_x_minutes: 82 | one: menos de 1 minuto 83 | other: menos de %{count} minutos 84 | over_x_years: 85 | one: más de 1 año 86 | other: más de %{count} años 87 | x_seconds: 88 | one: 1 segundo 89 | other: "%{count} segundos" 90 | x_minutes: 91 | one: 1 minuto 92 | other: "%{count} minutos" 93 | x_days: 94 | one: 1 día 95 | other: "%{count} días" 96 | x_months: 97 | one: 1 mes 98 | other: "%{count} meses" 99 | x_years: 100 | one: 1 año 101 | other: "%{count} años" 102 | prompts: 103 | second: Segundo 104 | minute: Minuto 105 | hour: Hora 106 | day: Día 107 | month: Mes 108 | year: Año 109 | errors: 110 | format: "%{attribute} %{message}" 111 | messages: 112 | accepted: debe ser aceptado 113 | blank: no puede estar en blanco 114 | confirmation: no coincide 115 | empty: no puede estar vacío 116 | equal_to: debe ser igual a %{count} 117 | even: debe ser par 118 | exclusion: está reservado 119 | greater_than: debe ser mayor que %{count} 120 | greater_than_or_equal_to: debe ser mayor que o igual a %{count} 121 | inclusion: no está incluido en la lista 122 | invalid: no es válido 123 | less_than: debe ser menor que %{count} 124 | less_than_or_equal_to: debe ser menor que o igual a %{count} 125 | model_invalid: 'La validación falló: %{errors}' 126 | not_a_number: no es un número 127 | not_an_integer: debe ser un entero 128 | odd: debe ser impar 129 | other_than: debe ser distinto de %{count} 130 | present: debe estar en blanco 131 | required: debe existir 132 | taken: ya está en uso 133 | too_long: 134 | one: es demasiado largo (1 carácter máximo) 135 | other: es demasiado largo (%{count} caracteres máximo) 136 | too_short: 137 | one: es demasiado corto (1 carácter mínimo) 138 | other: es demasiado corto (%{count} caracteres mínimo) 139 | wrong_length: 140 | one: no tiene la longitud correcta (1 carácter exactos) 141 | other: no tiene la longitud correcta (%{count} caracteres exactos) 142 | template: 143 | body: 'Se encontraron problemas con los siguientes campos:' 144 | header: 145 | one: No se pudo guardar este/a %{model} porque se encontró 1 error 146 | other: No se pudo guardar este/a %{model} porque se encontraron %{count} errores 147 | helpers: 148 | select: 149 | prompt: Por favor seleccione 150 | submit: 151 | create: Crear %{model} 152 | submit: Guardar %{model} 153 | update: Actualizar %{model} 154 | number: 155 | currency: 156 | format: 157 | delimiter: "." 158 | format: "%n %u" 159 | precision: 2 160 | separator: "," 161 | significant: false 162 | strip_insignificant_zeros: false 163 | unit: "€" 164 | format: 165 | delimiter: "." 166 | precision: 3 167 | separator: "," 168 | significant: false 169 | strip_insignificant_zeros: false 170 | human: 171 | decimal_units: 172 | format: "%n %u" 173 | units: 174 | billion: mil millones 175 | million: 176 | one: millón 177 | other: millones 178 | quadrillion: mil billones 179 | thousand: mil 180 | trillion: 181 | one: billón 182 | other: billones 183 | unit: '' 184 | format: 185 | delimiter: '' 186 | precision: 1 187 | significant: true 188 | strip_insignificant_zeros: true 189 | storage_units: 190 | format: "%n %u" 191 | units: 192 | byte: 193 | one: Byte 194 | other: Bytes 195 | eb: EB 196 | gb: GB 197 | kb: KB 198 | mb: MB 199 | pb: PB 200 | tb: TB 201 | percentage: 202 | format: 203 | delimiter: '' 204 | format: "%n %" 205 | precision: 206 | format: 207 | delimiter: '' 208 | support: 209 | array: 210 | last_word_connector: " y " 211 | two_words_connector: " y " 212 | words_connector: ", " 213 | time: 214 | am: am 215 | formats: 216 | default: "%A, %-d de %B de %Y %H:%M:%S %z" 217 | long: "%-d de %B de %Y %H:%M" 218 | short: "%-d de %b %H:%M" 219 | pm: pm 220 | 221 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5) 8 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 12 | # 13 | port ENV.fetch('PORT', 3000) 14 | 15 | # Specifies the `environment` that Puma will run in. 16 | # 17 | environment ENV.fetch('RAILS_ENV') { 'development' } 18 | 19 | # Specifies the number of `workers` to boot in clustered mode. 20 | # Workers are forked web server processes. If using threads and workers together 21 | # the concurrency of the application would be max `threads` * `workers`. 22 | # Workers do not work on JRuby or Windows (both of which do not support 23 | # processes). 24 | # 25 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 26 | 27 | # Use the `preload_app!` method when specifying a `workers` number. 28 | # This directive tells Puma to first boot the application and load code 29 | # before forking the application. This takes advantage of Copy On Write 30 | # process behavior so workers use less memory. 31 | # 32 | # preload_app! 33 | 34 | # Allow puma to be restarted by `rails restart` command. 35 | plugin :tmp_restart 36 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | require 'sidekiq/web' 2 | 3 | Rails.application.routes.draw do 4 | mount Sidekiq::Web => '/jobmonitor' 5 | 6 | devise_for :admin_users, ActiveAdmin::Devise.config 7 | ActiveAdmin.routes(self) 8 | 9 | mount_devise_token_auth_for 'User', at: '/api/v1/users', controllers: { 10 | registrations: 'api/v1/registrations', 11 | sessions: 'api/v1/sessions', 12 | passwords: 'api/v1/passwords', 13 | token_validations: 'api/v1/token_validations' 14 | }, skip: %i[omniauth_callbacks registrations] 15 | 16 | namespace :api, defaults: { format: :json } do 17 | namespace :v1 do 18 | resource :user, only: %i[show update] 19 | 20 | devise_scope :user do 21 | resources :users, only: [] do 22 | controller :registrations do 23 | post :create, on: :collection 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | :concurrency: 15 2 | :queues: 3 | - default 4 | - mailers 5 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | Spring.watch( 2 | '.ruby-version', 3 | '.rbenv-vars', 4 | 'tmp/restart.txt', 5 | 'tmp/caching-dev.txt' 6 | ) 7 | -------------------------------------------------------------------------------- /config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket 23 | 24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/migrate/20190703155941_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[6.0] 2 | def change 3 | create_table :users do |t| 4 | t.string :first_name 5 | t.string :last_name 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20190705213619_devise_token_auth_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseTokenAuthCreateUsers < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | change_table :users, bulk: true do |t| 6 | t.string :provider, null: false, default: 'email' 7 | t.string :email, null: false 8 | t.string :uid, null: false, default: nil 9 | t.string :encrypted_password, null: false, default: nil 10 | t.string :reset_password_token 11 | t.datetime :reset_password_sent_at 12 | t.datetime :remember_created_at 13 | t.string :confirmation_token 14 | t.datetime :confirmed_at 15 | t.datetime :confirmation_sent_at 16 | t.datetime :current_sign_in_at 17 | t.datetime :last_sign_in_at 18 | t.string :current_sign_in_ip 19 | t.string :last_sign_in_ip 20 | t.string :unconfirmed_email 21 | t.integer :sign_in_count, default: 0 22 | t.json :tokens, null: false, default: {} 23 | t.boolean :must_change_password, default: false 24 | end 25 | 26 | add_index :users, :email, unique: true, algorithm: :concurrently 27 | add_index :users, %i[uid provider], unique: true, algorithm: :concurrently 28 | add_index :users, :reset_password_token, unique: true, algorithm: :concurrently 29 | add_index :users, :confirmation_token, unique: true, algorithm: :concurrently 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /db/migrate/20200117193722_devise_create_admin_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateAdminUsers < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | create_table :admin_users do |t| 6 | t.string :email, null: false 7 | t.string :encrypted_password, null: false 8 | t.string :reset_password_token 9 | t.datetime :reset_password_sent_at 10 | t.datetime :remember_created_at 11 | t.timestamps null: false 12 | end 13 | 14 | add_index :admin_users, :email, unique: true, algorithm: :concurrently 15 | add_index :admin_users, :reset_password_token, unique: true, algorithm: :concurrently 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20200309184444_add_locale_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddLocaleToUsers < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :users, :locale, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `rails 6 | # db:schema:load`. When creating a new database, `rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2020_03_09_184444) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "admin_users", force: :cascade do |t| 19 | t.string "email", null: false 20 | t.string "encrypted_password", null: false 21 | t.string "reset_password_token" 22 | t.datetime "reset_password_sent_at" 23 | t.datetime "remember_created_at" 24 | t.datetime "created_at", precision: 6, null: false 25 | t.datetime "updated_at", precision: 6, null: false 26 | t.index ["email"], name: "index_admin_users_on_email", unique: true 27 | t.index ["reset_password_token"], name: "index_admin_users_on_reset_password_token", unique: true 28 | end 29 | 30 | create_table "users", force: :cascade do |t| 31 | t.string "first_name" 32 | t.string "last_name" 33 | t.datetime "created_at", precision: 6, null: false 34 | t.datetime "updated_at", precision: 6, null: false 35 | t.string "provider", default: "email", null: false 36 | t.string "email", null: false 37 | t.string "uid", null: false 38 | t.string "encrypted_password", null: false 39 | t.string "reset_password_token" 40 | t.datetime "reset_password_sent_at" 41 | t.datetime "remember_created_at" 42 | t.string "confirmation_token" 43 | t.datetime "confirmed_at" 44 | t.datetime "confirmation_sent_at" 45 | t.datetime "current_sign_in_at" 46 | t.datetime "last_sign_in_at" 47 | t.string "current_sign_in_ip" 48 | t.string "last_sign_in_ip" 49 | t.string "unconfirmed_email" 50 | t.integer "sign_in_count", default: 0 51 | t.json "tokens", default: {}, null: false 52 | t.boolean "must_change_password", default: false 53 | t.string "locale" 54 | t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true 55 | t.index ["email"], name: "index_users_on_email", unique: true 56 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 57 | t.index ["uid", "provider"], name: "index_users_on_uid_and_provider", unique: true 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This is not the place for test data 2 | # Only use this to put the necessary setup for the app to work 3 | # Separate the seeds in different Seed Service Objects 4 | # The data can then be loaded with the rails db:seed command 5 | 6 | unless AdminUser.count.positive? 7 | AdminUser.create!(email: 'admin@example.com', 8 | password: 'password', 9 | password_confirmation: 'password') 10 | end 11 | -------------------------------------------------------------------------------- /lib/gem_extensions/devise/token_generator.rb: -------------------------------------------------------------------------------- 1 | module GemExtensions 2 | module Devise 3 | module TokenGenerator 4 | SHORT_TOKEN_COLUMNS = [:reset_password_token].freeze 5 | 6 | def generate(klass, column) 7 | key = key_for(column) 8 | 9 | loop do 10 | raw = short_token?(column) ? short_raw_token : long_raw_token 11 | enc = OpenSSL::HMAC.hexdigest(@digest, key, raw) 12 | break [raw, enc] unless klass.to_adapter.find_first(column => enc) 13 | end 14 | end 15 | 16 | private 17 | 18 | def short_token?(column) 19 | SHORT_TOKEN_COLUMNS.include?(column) 20 | end 21 | 22 | def short_raw_token 23 | rand(100_000..999_999).to_s 24 | end 25 | 26 | def long_raw_token 27 | Devise.friendly_token 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/tasks/auto_annotate_models.rake: -------------------------------------------------------------------------------- 1 | if Rails.env.development? 2 | require 'annotate' 3 | task :set_annotation_options do 4 | # You can override any of these by setting an environment variable of the 5 | # same name. 6 | Annotate.set_defaults( 7 | 'additional_file_patterns' => [], 8 | 'routes' => 'false', 9 | 'models' => 'true', 10 | 'position_in_routes' => 'before', 11 | 'position_in_class' => 'before', 12 | 'position_in_test' => 'before', 13 | 'position_in_fixture' => 'before', 14 | 'position_in_factory' => 'before', 15 | 'position_in_serializer' => 'before', 16 | 'show_foreign_keys' => 'true', 17 | 'show_complete_foreign_keys' => 'false', 18 | 'show_indexes' => 'true', 19 | 'simple_indexes' => 'true', 20 | 'model_dir' => 'app/models', 21 | 'root_dir' => '', 22 | 'include_version' => 'false', 23 | 'require' => '', 24 | 'exclude_tests' => 'false', 25 | 'exclude_fixtures' => 'false', 26 | 'exclude_factories' => 'false', 27 | 'exclude_serializers' => 'false', 28 | 'exclude_scaffolds' => 'true', 29 | 'exclude_controllers' => 'true', 30 | 'exclude_helpers' => 'true', 31 | 'exclude_sti_subclasses' => 'false', 32 | 'ignore_model_sub_dir' => 'false', 33 | 'ignore_columns' => nil, 34 | 'ignore_routes' => nil, 35 | 'ignore_unknown_models' => 'false', 36 | 'hide_limit_column_types' => 'integer,bigint,boolean', 37 | 'hide_default_column_types' => 'json,jsonb,hstore,text', 38 | 'skip_on_db_migrate' => 'false', 39 | 'format_bare' => 'true', 40 | 'format_rdoc' => 'false', 41 | 'format_markdown' => 'false', 42 | 'sort' => 'false', 43 | 'force' => 'false', 44 | 'frozen' => 'false', 45 | 'classified_sort' => 'true', 46 | 'trace' => 'false', 47 | 'wrapper_open' => nil, 48 | 'wrapper_close' => nil, 49 | 'with_comment' => 'true' 50 | ) 51 | end 52 | 53 | Annotate.load_tasks 54 | end 55 | -------------------------------------------------------------------------------- /lib/tasks/linters.rake: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | task :linters do 4 | options = {} 5 | OptionParser.new { |opts| 6 | opts.on('-a') { |autofix| options[:autofix] = autofix } 7 | }.parse! 8 | 9 | sh "bundle exec rubocop --force-exclusion #{'-a' if options[:autofix]}" 10 | sh 'bundle exec reek --force-exclusion -c .reek.yml' 11 | end 12 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/log/.keep -------------------------------------------------------------------------------- /public/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/public/assets/favicon.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /release_tasks.sh: -------------------------------------------------------------------------------- 1 | echo "Running Release Tasks" 2 | 3 | if [ "$MIGRATIONS_ON_RELEASE" == "true" ]; then 4 | echo "Running Migrations" 5 | bundle exec rails db:migrate 6 | fi 7 | 8 | if [ "$SEED_ON_RELEASE" == "true" ]; then 9 | echo "Seeding DB" 10 | bundle exec rails db:seed 11 | fi 12 | 13 | echo "Done running release_tasks.sh" 14 | -------------------------------------------------------------------------------- /rubocop/cop/migration/add_index.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../migration_helpers' 2 | 3 | module RuboCop 4 | module Cop 5 | module Migration 6 | # Cop that checks if indexes are added in a concurrent manner. 7 | class AddIndex < RuboCop::Cop::Cop 8 | include MigrationHelpers 9 | 10 | MSG_DDT = 'Prefer using disable_ddl_transaction! and { algorithm: :concurrently }'\ 11 | ' when creating an index'.freeze 12 | MSG_ALG = 'Prefer using { algorithm: :concurrently } when creating an index'.freeze 13 | 14 | def_node_search :disable_ddl_transaction?, '(send $_ :disable_ddl_transaction!)' 15 | 16 | def_node_matcher :indexes?, <<-MATCHER 17 | (send _ {:add_index :drop_index} $...) 18 | MATCHER 19 | 20 | def on_class(node) 21 | return unless in_migration?(node) 22 | 23 | return if ddl_transaction_disabled? 24 | 25 | @disable_ddl_transaction = disable_ddl_transaction?(node) 26 | @offensive_node = node 27 | end 28 | 29 | def on_send(node) 30 | return unless in_migration?(node) 31 | 32 | indexes?(node) do |args| 33 | add_offense(node, message: MSG_ALG) unless concurrently_enabled?(args.last) 34 | add_offense(@offensive_node, message: MSG_DDT) unless ddl_transaction_disabled? 35 | end 36 | end 37 | 38 | private 39 | 40 | def concurrently_enabled?(last_arg) 41 | last_arg.hash_type? && last_arg.each_descendant.any? do |node| 42 | next unless node.sym_type? 43 | 44 | concurrently?(node) 45 | end 46 | end 47 | 48 | def ddl_transaction_disabled? 49 | @disable_ddl_transaction 50 | end 51 | 52 | def concurrently?(node) 53 | node.children.any? { |s| s == :concurrently } 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /rubocop/migration_helpers.rb: -------------------------------------------------------------------------------- 1 | module RuboCop 2 | # Module containing helper methods for writing migration cops. 3 | module MigrationHelpers 4 | # Returns true if the given node originated from the db/migrate directory. 5 | def in_migration?(node) 6 | dirname(node).end_with?('db/migrate') 7 | end 8 | 9 | private 10 | 11 | def dirname(node) 12 | File.dirname(node.location.expression.source_buffer.name) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /rubocop/rubocop.rb: -------------------------------------------------------------------------------- 1 | require_relative 'cop/migration/add_index' 2 | -------------------------------------------------------------------------------- /rubocop/rubocop_rails.yml: -------------------------------------------------------------------------------- 1 | Rails/LexicallyScopedActionFilter: 2 | Exclude: 3 | - app/controllers/**/** 4 | 5 | Rails/RakeEnvironment: 6 | Exclude: 7 | - lib/tasks/linters.rake 8 | - lib/tasks/auto_annotate_models.rake 9 | 10 | Rails/NotNullColumn: 11 | Enabled: false 12 | -------------------------------------------------------------------------------- /rubocop/rubocop_rspec.yml: -------------------------------------------------------------------------------- 1 | RSpec/ImplicitSubject: 2 | EnforcedStyle: single_statement_only 3 | -------------------------------------------------------------------------------- /spec/config/initializers/rack/attack_spec.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | describe Rack::Attack, type: :request do 4 | subject(:request!) do 5 | post user_session_path, params: params, headers: headers, as: :json 6 | end 7 | 8 | let(:limit) { 5 } 9 | 10 | describe 'throttle excessive requests by email address' do 11 | let(:params) do 12 | { 13 | user: { 14 | email: 'wrong@email.com', 15 | password: 'wrong_password' 16 | } 17 | } 18 | end 19 | 20 | let(:headers) { {} } 21 | 22 | context 'when the number of requests is lower than the limit' do 23 | specify do 24 | within_limit_request do 25 | expect(response).not_to have_http_status(:too_many_requests) 26 | end 27 | end 28 | end 29 | 30 | context 'when the number of requests is higher than the limit' do 31 | specify do 32 | exceed_limit_request do 33 | expect(response).to have_http_status(:too_many_requests) 34 | end 35 | end 36 | 37 | specify do 38 | exceed_limit_request do 39 | expect(json[:errors]).to include('Throttle limit reached') 40 | end 41 | end 42 | end 43 | 44 | describe 'throttle excessive requests by IP address' do 45 | let(:params) do 46 | { 47 | user: { 48 | email: "wrong+#{SecureRandom.hex(5)}@email.com", 49 | password: 'wrong_password' 50 | } 51 | } 52 | end 53 | 54 | let(:headers) { { REMOTE_ADDR: '1.2.3.4' } } 55 | 56 | context 'when the number of requests is lower than the limit' do 57 | specify do 58 | within_limit_request do 59 | expect(response).not_to have_http_status(:too_many_requests) 60 | end 61 | end 62 | end 63 | 64 | context 'when the number of requests is higher than the limit' do 65 | specify do 66 | exceed_limit_request do 67 | expect(response).to have_http_status(:too_many_requests) 68 | end 69 | end 70 | 71 | specify do 72 | exceed_limit_request do 73 | expect(json[:errors]).to include('Throttle limit reached') 74 | end 75 | end 76 | end 77 | end 78 | 79 | def exceed_limit_request 80 | (limit + 1).times do |req_amount| 81 | request! 82 | 83 | yield if req_amount > limit 84 | end 85 | end 86 | 87 | def within_limit_request 88 | limit.times do 89 | request! 90 | 91 | yield 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/factories/admin_users.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: admin_users 4 | # 5 | # id :bigint not null, primary key 6 | # email :string not null, indexed 7 | # encrypted_password :string not null 8 | # remember_created_at :datetime 9 | # reset_password_sent_at :datetime 10 | # reset_password_token :string indexed 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | # Indexes 15 | # 16 | # index_admin_users_on_email (email) UNIQUE 17 | # index_admin_users_on_reset_password_token (reset_password_token) UNIQUE 18 | # 19 | 20 | FactoryBot.define do 21 | factory :admin_user do 22 | email { Faker::Internet.email } 23 | password { 'password123' } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # confirmation_sent_at :datetime 7 | # confirmation_token :string indexed 8 | # confirmed_at :datetime 9 | # current_sign_in_at :datetime 10 | # current_sign_in_ip :string 11 | # email :string not null, indexed 12 | # encrypted_password :string not null 13 | # first_name :string 14 | # last_name :string 15 | # last_sign_in_at :datetime 16 | # last_sign_in_ip :string 17 | # locale :string 18 | # must_change_password :boolean default(FALSE) 19 | # provider :string default("email"), not null, indexed => [uid] 20 | # remember_created_at :datetime 21 | # reset_password_sent_at :datetime 22 | # reset_password_token :string indexed 23 | # sign_in_count :integer default(0) 24 | # tokens :json not null 25 | # uid :string not null, indexed => [provider] 26 | # unconfirmed_email :string 27 | # created_at :datetime not null 28 | # updated_at :datetime not null 29 | # 30 | # Indexes 31 | # 32 | # index_users_on_confirmation_token (confirmation_token) UNIQUE 33 | # index_users_on_email (email) UNIQUE 34 | # index_users_on_reset_password_token (reset_password_token) UNIQUE 35 | # index_users_on_uid_and_provider (uid,provider) UNIQUE 36 | # 37 | 38 | FactoryBot.define do 39 | factory :user do 40 | first_name { Faker::Name.first_name } 41 | last_name { Faker::Name.last_name } 42 | email { Faker::Internet.email } 43 | password { 'abcd1234' } 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/mailers/previews/devise_mailer_preview.rb: -------------------------------------------------------------------------------- 1 | class DeviseMailerPreview < ActionMailer::Preview 2 | def reset_password_instructions 3 | user = FactoryBot.build(:user) 4 | token = Faker::Number.number(digits: 6) 5 | Devise::Mailer.reset_password_instructions(user, token) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/mailers/previews/reset_password_preview.rb: -------------------------------------------------------------------------------- 1 | class ResetPasswordPreview < ActionMailer::Preview 2 | def reset_password_email 3 | UserMailer.with(user: FactoryBot.build(:user), 4 | new_password: Faker::Internet.password(8)) 5 | .reset_password_email 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/models/admin_user_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: admin_users 4 | # 5 | # id :bigint not null, primary key 6 | # email :string not null, indexed 7 | # encrypted_password :string not null 8 | # remember_created_at :datetime 9 | # reset_password_sent_at :datetime 10 | # reset_password_token :string indexed 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | # Indexes 15 | # 16 | # index_admin_users_on_email (email) UNIQUE 17 | # index_admin_users_on_reset_password_token (reset_password_token) UNIQUE 18 | # 19 | 20 | RSpec.describe AdminUser, type: :model do 21 | describe 'validations' do 22 | it { is_expected.to validate_presence_of(:email) } 23 | it { is_expected.to validate_presence_of(:password) } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: users 4 | # 5 | # id :bigint not null, primary key 6 | # confirmation_sent_at :datetime 7 | # confirmation_token :string indexed 8 | # confirmed_at :datetime 9 | # current_sign_in_at :datetime 10 | # current_sign_in_ip :string 11 | # email :string not null, indexed 12 | # encrypted_password :string not null 13 | # first_name :string 14 | # last_name :string 15 | # last_sign_in_at :datetime 16 | # last_sign_in_ip :string 17 | # locale :string 18 | # must_change_password :boolean default(FALSE) 19 | # provider :string default("email"), not null, indexed => [uid] 20 | # remember_created_at :datetime 21 | # reset_password_sent_at :datetime 22 | # reset_password_token :string indexed 23 | # sign_in_count :integer default(0) 24 | # tokens :json not null 25 | # uid :string not null, indexed => [provider] 26 | # unconfirmed_email :string 27 | # created_at :datetime not null 28 | # updated_at :datetime not null 29 | # 30 | # Indexes 31 | # 32 | # index_users_on_confirmation_token (confirmation_token) UNIQUE 33 | # index_users_on_email (email) UNIQUE 34 | # index_users_on_reset_password_token (reset_password_token) UNIQUE 35 | # index_users_on_uid_and_provider (uid,provider) UNIQUE 36 | # 37 | 38 | describe User, type: :model do 39 | subject(:user) { create(:user) } 40 | 41 | describe 'validations' do 42 | it { is_expected.to validate_presence_of(:email) } 43 | it { is_expected.to validate_uniqueness_of(:email).scoped_to(:provider).case_insensitive } 44 | 45 | specify do 46 | is_expected.to validate_inclusion_of(:locale).in_array(I18n.available_locales.map(&:to_s)) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../config/environment', __dir__) 3 | 4 | abort('The Rails environment is running in production mode!') if Rails.env.production? 5 | 6 | require 'rspec/rails' 7 | require 'spec_helper' 8 | 9 | Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |file| require file } 10 | 11 | begin 12 | ActiveRecord::Migration.maintain_test_schema! 13 | rescue ActiveRecord::PendingMigrationError => e 14 | puts e.to_s.strip 15 | exit 1 16 | end 17 | 18 | RSpec.configure do |config| 19 | config.include RequestHelpers, type: :request 20 | config.include_context 'request initializer', type: :request 21 | 22 | config.include Rails.application.routes.url_helpers 23 | 24 | config.use_transactional_fixtures = true 25 | config.infer_spec_type_from_file_location! 26 | config.filter_rails_from_backtrace! 27 | end 28 | -------------------------------------------------------------------------------- /spec/requests/api/v1/passwords/create_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'POST /api/v1/users/password', { type: :request } do 2 | let(:request!) { post user_password_path, params: params, as: :json } 3 | let(:user) { create(:user, password: 'old password') } 4 | 5 | context 'with an email' do 6 | context 'when the email exists' do 7 | let(:params) { { email: user.email } } 8 | 9 | include_examples 'have http status', :no_content 10 | 11 | it 'sends an email with the reset password token', skip_request: true do 12 | expect { request! }.to have_enqueued_email(Devise::Mailer, :reset_password_instructions) 13 | end 14 | 15 | it 'changes the user reset password token', skip_request: true do 16 | expect { request! }.to(change { user.reload.reset_password_token }) 17 | end 18 | end 19 | 20 | context 'when the email does not exist' do 21 | let(:params) { { email: 'wrong@email.com' } } 22 | 23 | include_examples 'have http status', :not_found 24 | 25 | it 'does not send an email', skip_request: true do 26 | expect { request! }.not_to(change { ActionMailer::Base.deliveries.count }) 27 | end 28 | end 29 | end 30 | 31 | context 'without an email' do 32 | let(:params) { { email: nil } } 33 | 34 | include_examples 'have http status', :unauthorized 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/requests/api/v1/passwords/edit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | 3 | describe 'GET api/v1/users/password/edit', { type: :request } do 4 | let(:request!) { get edit_user_password_path, params: params, headers: headers, as: :json } 5 | let(:params) { { reset_password_token: password_token } } 6 | let!(:password_token) { user.send(:set_reset_password_token) } 7 | let(:user) { create(:user) } 8 | 9 | context 'with a valid token' do 10 | include_examples 'have http status', :no_content 11 | end 12 | 13 | context 'with an expired token' do 14 | before do 15 | sent_at = user.reset_password_sent_at - Devise.reset_password_within - 1.second 16 | user.update!(reset_password_sent_at: sent_at) 17 | 18 | request! 19 | end 20 | 21 | include_examples 'have http status with error', 22 | :bad_request, 23 | I18n.t('errors.invalid_reset_password_token'), 24 | skip_request: true 25 | end 26 | 27 | context 'with an invalid token' do 28 | let(:password_token) { 'invalid token' } 29 | 30 | include_examples 'have http status with error', 31 | :bad_request, 32 | I18n.t('errors.invalid_reset_password_token') 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/requests/api/v1/passwords/update_spec.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | 3 | describe 'PUT api/v1/users/password/', { type: :request } do 4 | let(:request!) { put user_password_path, params: params, headers: headers, as: :json } 5 | let(:user) { create(:user) } 6 | let!(:password_token) { user.send(:set_reset_password_token) } 7 | let(:new_password) { '123456789aA?!' } 8 | let(:params) do 9 | { 10 | reset_password_token: password_token, 11 | password: new_password 12 | } 13 | end 14 | 15 | context 'with a valid token' do 16 | context 'with valid params' do 17 | it 'updates the user password', skip_request: true do 18 | expect { request! }.to(change { user.reload.encrypted_password }) 19 | end 20 | 21 | include_examples 'have http status', :ok 22 | 23 | it { expect(user.reload).to be_valid_password(new_password) } 24 | 25 | it 'returns the user data' do 26 | expect(json[:user]).to include_json(id: user.id, 27 | first_name: user.first_name, 28 | last_name: user.last_name, 29 | email: user.email) 30 | end 31 | 32 | it 'returns a valid client and access token' do 33 | token = response.header['access-token'] 34 | client = response.header['client'] 35 | 36 | expect(user.reload).to be_valid_token(token, client) 37 | end 38 | end 39 | 40 | context 'with invalid params' do 41 | let(:new_password) { nil } 42 | 43 | include_examples 'have http status', :unprocessable_entity 44 | 45 | it { expect(json[:attributes_errors]).not_to have_key('reset_password_token') } 46 | 47 | it 'does not change the user password', skip_request: true do 48 | expect { request! }.not_to change(user.reload, :encrypted_password) 49 | end 50 | end 51 | end 52 | 53 | context 'with an invalid token' do 54 | let(:password_token) { 'invalid token' } 55 | 56 | include_examples 'have http status', :bad_request 57 | 58 | it { expect(user.reload).not_to be_valid_password(new_password) } 59 | 60 | it 'does not change the user password', skip_request: true do 61 | expect { request! }.not_to change(user.reload, :encrypted_password) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/requests/api/v1/sessions/create_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'POST /api/v1/users/sign_in', { type: :request } do 2 | let(:request!) { post user_session_path, params: params, as: :json } 3 | let(:params) do 4 | { 5 | user: { 6 | email: email, 7 | password: password 8 | } 9 | } 10 | end 11 | let(:email) { user.email } 12 | let(:password) { user.password } 13 | let(:user) { create(:user, password: 'password', email: 'user@mail.com') } 14 | 15 | context 'with an existing email and password' do 16 | include_examples 'have http status', :ok 17 | 18 | specify do 19 | expect(json[:user]).to include_json( 20 | id: user.id, 21 | first_name: user.first_name, 22 | last_name: user.last_name, 23 | email: user.email, 24 | locale: user.locale 25 | ) 26 | end 27 | 28 | it 'returns if the user needs to change the password' do 29 | expect(json[:must_change_password]).to eq(user.must_change_password) 30 | end 31 | 32 | it 'returns a valid client and access token' do 33 | token = response.header['access-token'] 34 | client = response.header['client'] 35 | 36 | expect(user.reload).to be_valid_token(token, client) 37 | end 38 | end 39 | 40 | context 'when the password is invalid for the given email' do 41 | let(:password) { 'wrong_password' } 42 | 43 | include_examples 'have http status', :forbidden 44 | end 45 | 46 | context 'when the email does not exist' do 47 | let(:email) { 'wrong@email.com' } 48 | 49 | include_examples 'have http status', :forbidden 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/requests/api/v1/sessions/destroy_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'DELETE /api/v1/users/sign_out', { type: :request } do 2 | let(:request!) { delete destroy_user_session_path, headers: headers, as: :json } 3 | let(:user) { create(:user) } 4 | 5 | context 'when being signed in' do 6 | let(:headers) { auth_headers } 7 | 8 | include_examples 'have http status', :no_content 9 | 10 | it 'destroys the user token', skip_request: true do 11 | access_token = auth_headers['access-token'] 12 | client = auth_headers['client'] 13 | 14 | expect { 15 | request! 16 | }.to change { user.reload.valid_token?(access_token, client) }.from(true).to(false) 17 | end 18 | end 19 | 20 | context 'when not being signed in' do 21 | let(:headers) { nil } 22 | 23 | include_examples 'have http status with error', 24 | :unauthorized, 25 | I18n.t('devise.failure.unauthenticated') 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/requests/api/v1/token_validations/validate_token_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'GET /api/v1/users/validate_token', { type: :request } do 2 | let(:request!) { get api_v1_users_validate_token_path, headers: headers, as: :json } 3 | let(:user) { create(:user) } 4 | 5 | context 'when being signed in' do 6 | let(:headers) { auth_headers } 7 | 8 | include_examples 'have http status', :ok 9 | 10 | specify do 11 | expect(json[:user]).to include_json( 12 | id: user.id, 13 | first_name: user.first_name, 14 | last_name: user.last_name, 15 | email: user.email, 16 | locale: user.locale 17 | ) 18 | end 19 | 20 | it 'returns valid authentication headers' do 21 | token = response.header['access-token'] 22 | client = response.header['client'] 23 | 24 | expect(user.reload).to be_valid_token(token, client) 25 | end 26 | end 27 | 28 | context 'when not being signed in' do 29 | let(:headers) { nil } 30 | 31 | include_examples 'have http status with error', 32 | :forbidden, 33 | I18n.t('errors.authentication.invalid_token') 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/requests/api/v1/users/create_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'POST /api/v1/users', { type: :request } do 2 | let(:request!) { post api_v1_users_path, params: params, as: :json } 3 | let(:email) { 'obikenobi@rebel.com' } 4 | let(:password) { 'abcd1234' } 5 | let(:params) do 6 | { 7 | user: { 8 | first_name: 'Obi Wan', 9 | last_name: 'Kenobi', 10 | email: email, 11 | password: password, 12 | locale: 'es' 13 | } 14 | } 15 | end 16 | 17 | context 'with correct params given' do 18 | let(:created_user) { User.last } 19 | 20 | include_examples 'have http status', :ok 21 | 22 | specify do 23 | expect(json[:user]).to include_json( 24 | id: created_user.id, 25 | first_name: created_user.first_name, 26 | last_name: created_user.last_name, 27 | email: created_user.email, 28 | locale: created_user.locale 29 | ) 30 | end 31 | 32 | it 'sets the authentication headers' do 33 | token = response.header['access-token'] 34 | client = response.header['client'] 35 | 36 | expect(created_user.reload).to be_valid_token(token, client) 37 | end 38 | end 39 | 40 | context 'with invalid params' do 41 | shared_examples 'missing parameter' do |field| 42 | let(field) { nil } 43 | 44 | include_examples 'have http status', :unprocessable_entity 45 | 46 | it { expect(json.dig(:attributes_errors, field)).to include("can't be blank") } 47 | end 48 | 49 | context 'when the email is missing' do 50 | include_examples 'missing parameter', :email 51 | end 52 | 53 | context 'when the password is missing' do 54 | include_examples 'missing parameter', :password 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/requests/api/v1/users/show_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'GET /api/v1/user', { type: :request } do 2 | let(:request!) { get api_v1_user_path, headers: headers, as: :json } 3 | let(:user) { create(:user) } 4 | 5 | context 'when being signed in' do 6 | let(:headers) { auth_headers } 7 | 8 | include_examples 'have http status', :ok 9 | 10 | specify do 11 | expect(json[:user]).to include_json( 12 | id: user.id, 13 | first_name: user.first_name, 14 | last_name: user.last_name, 15 | email: user.email, 16 | locale: user.locale 17 | ) 18 | end 19 | end 20 | 21 | context 'when not being signed in' do 22 | it_behaves_like 'an authenticated endpoint' 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/requests/api/v1/users/update_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'PUT /api/v1/user', { type: :request } do 2 | let(:request!) { put api_v1_user_path, params: params, headers: headers, as: :json } 3 | 4 | context 'when being signed in' do 5 | let(:headers) { auth_headers } 6 | 7 | before { user.reload } 8 | 9 | context 'when not changing the password' do 10 | let(:user) { create(:user) } 11 | let(:params) do 12 | { 13 | user: { 14 | first_name: 'Darth', 15 | last_name: 'Vader', 16 | locale: 'es' 17 | } 18 | } 19 | end 20 | 21 | include_examples 'have http status', :ok 22 | 23 | specify do 24 | expect(json[:user]).to include_json( 25 | first_name: 'Darth', 26 | last_name: 'Vader', 27 | locale: 'es' 28 | ) 29 | end 30 | end 31 | 32 | context 'when changing the password' do 33 | let(:user) { create(:user, password: 'oldPassword') } 34 | 35 | shared_examples 'invalid password' do 36 | include_examples 'have http status with error', 37 | :bad_request, 38 | 'The current password is not valid' 39 | 40 | it { expect(user.valid_password?('oldPassword')).to be(true) } 41 | it { expect(user.valid_password?('newPassword')).to be(false) } 42 | end 43 | 44 | context 'without the password check' do 45 | let(:params) { { user: { password: 'newPassword' } } } 46 | 47 | include_examples 'invalid password' 48 | end 49 | 50 | context 'with the password check' do 51 | let(:params) { { user: { password: 'newPassword' }, password_check: password_check } } 52 | 53 | context 'with an invalid password check' do 54 | let(:password_check) { 'notThePassword' } 55 | 56 | include_examples 'invalid password' 57 | end 58 | 59 | context 'with the correct password check' do 60 | let(:password_check) { 'oldPassword' } 61 | 62 | include_examples 'have http status', :ok 63 | 64 | it { expect(user.valid_password?('newPassword')).to be(true) } 65 | 66 | specify do 67 | expect(json[:user]).to include_json( 68 | id: user.id, 69 | first_name: user.first_name, 70 | last_name: user.last_name, 71 | email: user.email, 72 | locale: user.locale 73 | ) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | context 'when not being signed in' do 81 | let(:params) { nil } 82 | 83 | it_behaves_like 'an authenticated endpoint' 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/rubocop/cop/migration/add_index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | require 'rubocop' 6 | require 'rubocop/rspec/support' 7 | 8 | require_relative '../../../../rubocop/cop/migration/add_index' 9 | 10 | RSpec.describe RuboCop::Cop::Migration::AddIndex do 11 | include CopHelper 12 | include RuboCop::RSpec::ExpectOffense 13 | 14 | let(:cop) { described_class.new } 15 | 16 | before { allow(cop).to receive(:in_migration?).and_return(true) } 17 | 18 | context 'with concurrently and without disable_ddl_transaction!' do 19 | it 'reports an offense' do 20 | expect_offense(<<~RUBY) 21 | class Migration 22 | ^^^^^^^^^^^^^^^ Prefer using disable_ddl_transaction! and { algorithm: :concurrently } when creating an index 23 | def change 24 | add_index :table, :column, algorithm: :concurrently 25 | end 26 | end 27 | RUBY 28 | end 29 | end 30 | 31 | context 'without concurrently and without disable_ddl_transaction!' do 32 | it 'reports an offense' do 33 | expect_offense(<<~RUBY) 34 | class Migration 35 | ^^^^^^^^^^^^^^^ Prefer using disable_ddl_transaction! and { algorithm: :concurrently } when creating an index 36 | def change 37 | add_index :table, :column 38 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using { algorithm: :concurrently } when creating an index 39 | end 40 | end 41 | RUBY 42 | end 43 | end 44 | 45 | context 'with concurrently and with disable_ddl_transaction!' do 46 | it 'does not report an offense' do 47 | expect_no_offenses(<<~RUBY) 48 | class Migration 49 | disable_ddl_transaction! 50 | def change 51 | add_index :table, :column, algorithm: :concurrently 52 | end 53 | end 54 | RUBY 55 | end 56 | end 57 | 58 | context 'without concurrently and with disable_ddl_transaction!' do 59 | it 'reports an offense' do 60 | expect_offense(<<~RUBY) 61 | class Migration 62 | disable_ddl_transaction! 63 | def change 64 | add_index :table, :column 65 | ^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer using { algorithm: :concurrently } when creating an index 66 | end 67 | end 68 | RUBY 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_job' 2 | require 'webmock/rspec' 3 | require 'active_support/testing/time_helpers' 4 | 5 | RSpec.configure do |config| 6 | config.include ActiveJob::TestHelper 7 | config.include ActiveSupport::Testing::TimeHelpers 8 | 9 | config.expect_with :rspec do |expectations| 10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 11 | expectations.syntax = :expect 12 | end 13 | 14 | config.mock_with :rspec do |mocks| 15 | mocks.verify_partial_doubles = true 16 | end 17 | 18 | config.shared_context_metadata_behavior = :apply_to_host_groups 19 | config.order = :random 20 | 21 | config.before do 22 | ActionMailer::Base.deliveries.clear 23 | ActiveJob::Base.queue_adapter = :test 24 | end 25 | 26 | config.after do 27 | FileUtils.rm_rf(Dir[Rails.root.join('/spec/support/uploads').to_s]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/factory_bot.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryBot::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module RequestHelpers 2 | def json 3 | raise 'Response is nil. Are you sure you made a request?' unless response 4 | 5 | JSON.parse(response.body, symbolize_names: true) 6 | end 7 | 8 | def auth_headers 9 | @auth_headers ||= user.create_new_auth_token 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/shared_context/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../../../.rubocop.yml 2 | 3 | RSpec/ContextWording: 4 | Enabled: false 5 | -------------------------------------------------------------------------------- /spec/support/shared_context/request_initializer.rb: -------------------------------------------------------------------------------- 1 | shared_context 'request initializer' do 2 | subject { response } 3 | 4 | before { |example| request! unless example.metadata[:skip_request] } 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/shared_examples/authentication.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'an authenticated endpoint' do 2 | let(:headers) { nil } 3 | 4 | include_examples 'have http status with error', 5 | :unauthorized, 6 | I18n.t('devise.failure.unauthenticated') 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/shared_examples/have_http_status.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'have http status' do |status, skip_request: false| 2 | specify '', skip_request: skip_request do 3 | is_expected.to have_http_status(status) 4 | end 5 | end 6 | 7 | shared_examples 'have http status with error' do |status, error, skip_request: false| 8 | include_examples 'have http status', status, skip_request: skip_request 9 | 10 | specify '', skip_request: skip_request do 11 | expect(json[:errors]).to include(error) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/simple_cov.rb: -------------------------------------------------------------------------------- 1 | SimpleCov.start 2 | -------------------------------------------------------------------------------- /spec/support/webmock.rb: -------------------------------------------------------------------------------- 1 | require 'webmock/rspec' 2 | 3 | WebMock.disable_net_connect!(allow_localhost: true) 4 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/storage/.keep -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/tmp/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loopstudio/rails-api-boilerplate/ffcf31695195b90c1db0b1aee9cf0f1501d2bfb1/vendor/.keep --------------------------------------------------------------------------------