├── .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 | 
2 |
3 |
Start your next Rails 6 API project in seconds
4 |
5 | 
6 | [](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
--------------------------------------------------------------------------------