├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Procfile ├── Procfile.dev ├── README.md ├── Rakefile ├── app ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── contracts │ ├── application_contract.rb │ └── users │ │ ├── passwords │ │ ├── send_instructions_contract.rb │ │ └── update_contract.rb │ │ └── registrations │ │ └── register_contract.rb ├── controllers │ ├── application_controller.rb │ ├── authenticated_controller.rb │ ├── concerns │ │ ├── doorkeeper │ │ │ └── authorize.rb │ │ └── parameter_objects.rb │ ├── swagger_controller.rb │ └── v1 │ │ └── users │ │ ├── passwords_controller.rb │ │ ├── registrations_controller.rb │ │ └── tokens_controller.rb ├── helpers │ └── .keep ├── jobs │ └── application_job.rb ├── lib │ ├── custom_objects │ │ ├── base.rb │ │ └── parameter_object │ │ │ ├── pagination.rb │ │ │ └── query.rb │ └── types.rb ├── mailers │ └── application_mailer.rb ├── models │ ├── application_record.rb │ ├── concerns │ │ └── authenticable.rb │ └── user.rb ├── operations │ ├── application_operation.rb │ └── users │ │ ├── passwords │ │ ├── create_operation.rb │ │ └── update_operation.rb │ │ └── registrations │ │ └── create_operation.rb ├── services │ ├── application_service.rb │ ├── concerns │ │ └── .keep │ ├── doorkeeper │ │ └── access_tokens │ │ │ └── create_service.rb │ └── users │ │ ├── create_service.rb │ │ ├── passwords │ │ ├── send_instructions_service.rb │ │ └── update_service.rb │ │ └── registrations │ │ └── register_service.rb ├── swagger_docs │ ├── controllers │ │ └── v1 │ │ │ └── users │ │ │ ├── passwords_controller.rb │ │ │ ├── registrations_controller.rb │ │ │ └── tokens_controller.rb │ ├── inputs │ │ └── v1 │ │ │ └── user │ │ │ ├── reset_password_input.rb │ │ │ ├── revoke_input.rb │ │ │ ├── sign_in_input.rb │ │ │ ├── sign_up_input.rb │ │ │ └── update_password_input.rb │ ├── models │ │ └── shared │ │ │ ├── meta.rb │ │ │ └── pagination.rb │ ├── parameters │ │ └── shared │ │ │ └── .keep │ ├── responses │ │ ├── shared │ │ │ └── error_response.rb │ │ └── v1 │ │ │ └── user │ │ │ ├── reset_password_response.rb │ │ │ ├── sign_in_response.rb │ │ │ ├── sign_up_response.rb │ │ │ └── update_password_response.rb │ └── swagger_docs.rb └── views │ ├── devise │ └── mailer │ │ ├── confirmation_instructions.html.erb │ │ ├── email_changed.html.erb │ │ ├── password_change.html.erb │ │ ├── reset_password_instructions.html.erb │ │ └── unlock_instructions.html.erb │ ├── layouts │ ├── mailer.html.erb │ ├── mailer.text.erb │ └── swagger.html.erb │ └── shared │ └── partials │ ├── _meta.json.jbuilder │ └── _pagination.json.jbuilder ├── bin ├── bundle ├── dev ├── rails ├── rake └── setup ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── credentials.yml.enc ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── cors.rb │ ├── devise.rb │ ├── doorkeeper.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── kaminari_config.rb │ └── ransack.rb ├── locales │ ├── contracts │ │ └── tr.yml │ ├── defaults │ │ └── tr.yml │ ├── en.yml │ ├── gems │ │ ├── devise │ │ │ ├── en.yml │ │ │ └── tr.yml │ │ ├── doorkeeper │ │ │ ├── en.yml │ │ │ └── tr.yml │ │ └── dry_validation │ │ │ ├── en.yml │ │ │ └── tr.yml │ └── services │ │ ├── en.yml │ │ └── tr.yml ├── puma.rb ├── routes.rb ├── routes │ ├── authentication.rb │ └── swagger.rb ├── sidekiq.yml └── storage.yml ├── db ├── migrate │ ├── 20220212185845_devise_create_users.rb │ └── 20220212195921_create_doorkeeper_tables.rb ├── schema.rb └── seeds.rb ├── docs ├── CONTRACT.md ├── RANSACK.md ├── SERVICE.md ├── SWAGGER.md └── cover.png ├── lib ├── generators │ ├── contract │ │ ├── USAGE │ │ ├── contract_generator.rb │ │ └── templates │ │ │ ├── contract.rb.tt │ │ │ └── contract_test.rb.tt │ ├── operation │ │ ├── USAGE │ │ ├── operation_generator.rb │ │ └── templates │ │ │ ├── operation.rb.tt │ │ │ └── operation_test.rb.tt │ └── service │ │ ├── USAGE │ │ ├── service_generator.rb │ │ └── templates │ │ ├── service.rb.tt │ │ └── service_test.rb.tt ├── supports │ ├── application_contract │ │ ├── error_parser.rb │ │ └── i18n.rb │ ├── application_operation │ │ └── helper.rb │ ├── application_service │ │ └── helper.rb │ ├── doorkeeper │ │ ├── custom_error_response.rb │ │ ├── custom_register_response.rb │ │ └── custom_token_response.rb │ └── sidekiq │ │ └── helper.rb └── tasks │ └── .keep ├── log └── .keep ├── public ├── favicon.ico └── robots.txt ├── storage └── .keep ├── test ├── channels │ └── application_cable │ │ └── connection_test.rb ├── contracts │ └── users │ │ ├── passwords │ │ ├── send_instructions_contract_test.rb │ │ └── update_contract_test.rb │ │ └── registrations │ │ └── register_contract_test.rb ├── controllers │ ├── swagger_controller_test.rb │ └── v1 │ │ └── users │ │ ├── passwords_controller_test.rb │ │ ├── registrations_controller_test.rb │ │ └── tokens_controller_test.rb ├── factories │ ├── doorkeeper │ │ ├── doorkeeper_access_tokens_factory.rb │ │ └── doorkeeper_applications_factory.rb │ └── users_factory.rb ├── fixtures │ └── files │ │ └── .keep ├── helpers │ └── .keep ├── integration │ └── .keep ├── lib │ ├── custom_objects │ │ └── parameter_object │ │ │ ├── pagination_test.rb │ │ │ └── query_test.rb │ └── generators │ │ ├── contract_generator_test.rb │ │ ├── operation_generator_test.rb │ │ └── service_generator_test.rb ├── mailers │ └── .keep ├── models │ └── user_test.rb ├── operations │ └── users │ │ ├── passwords │ │ ├── create_operation_test.rb │ │ └── update_operation_test.rb │ │ └── registrations │ │ └── create_operation_test.rb ├── services │ ├── doorkeeper │ │ └── access_tokens │ │ │ └── create_service_test.rb │ └── users │ │ ├── create_service_test.rb │ │ ├── passwords │ │ ├── send_instructions_service_test.rb │ │ └── update_service_test.rb │ │ └── registrations │ │ └── register_service_test.rb ├── supports │ ├── body_parser.rb │ ├── contract_parser.rb │ ├── contract_validator.rb │ ├── doorkeeper_authenticator.rb │ └── sidekiq_minitest_support.rb └── test_helper.rb ├── tmp ├── .keep ├── pids │ └── .keep └── storage │ └── .keep └── vendor └── .keep /.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # build & test & quality 2 | 3 | name: build & test & quality 4 | 5 | # Triggers the workflow on push or pull request events 6 | on: [push] 7 | 8 | concurrency: 9 | group: ${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | minitest: 15 | env: 16 | POSTGRES_USER: postgres 17 | POSTGRES_PASSWORD: postgres 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: [ubuntu-latest] 23 | ruby-version: [3.1.2] 24 | 25 | # The type of runner that the job will run on 26 | runs-on: ${{ matrix.os }} 27 | 28 | services: 29 | redis: 30 | image: redis 31 | options: >- 32 | --health-cmd "redis-cli ping" 33 | --health-interval 10s 34 | --health-timeout 5s 35 | --health-retries 5 36 | ports: 37 | - 6379:6379 38 | 39 | # Label used to access the service container 40 | postgres: 41 | # Docker Hub image 42 | image: postgres:14 43 | # Provide the password for postgres 44 | env: 45 | POSTGRES_USER: postgres 46 | POSTGRES_PASSWORD: postgres 47 | # Set health checks to wait until postgres has started 48 | options: >- 49 | --health-cmd pg_isready 50 | --health-interval 10s 51 | --health-timeout 5s 52 | --health-retries 5 53 | ports: 54 | # Maps tcp port 5432 on service container to the host 55 | - 5432:5432 56 | 57 | # Steps represent a sequence of tasks that will be executed as part of the job 58 | steps: 59 | - run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 60 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 61 | - uses: actions/checkout@v2 62 | with: 63 | persist-credentials: false 64 | 65 | - name: Set up Ruby ${{ matrix.ruby-version }} 66 | uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: ${{ matrix.ruby-version }} 69 | bundler-cache: false 70 | 71 | - name: Install ruby dependencies 72 | run: bundle install 73 | 74 | - name: Create test database 75 | env: 76 | RAILS_ENV: test 77 | run: | 78 | bundle exec rails db:create 79 | bundle exec rails db:schema:load 80 | 81 | - name: Running rails tests 82 | run: bundle exec rails test 83 | 84 | rubocop: 85 | strategy: 86 | matrix: 87 | os: [ubuntu-latest] 88 | ruby-version: [3.1.2] 89 | 90 | runs-on: ${{ matrix.os }} 91 | 92 | steps: 93 | - uses: actions/checkout@v2 94 | with: 95 | persist-credentials: false 96 | 97 | - name: Set up Ruby ${{ matrix.ruby-version }} 98 | uses: ruby/setup-ruby@v1 99 | with: 100 | ruby-version: ${{ matrix.ruby-version }} 101 | bundler-cache: false 102 | 103 | - name: Install ruby dependencies 104 | run: bundle install 105 | 106 | - name: Rubocop 107 | run: bundle exec rubocop --config .rubocop.yml --parallel 108 | 109 | zeitwerk: 110 | env: 111 | RAILS_ENV: test 112 | 113 | strategy: 114 | matrix: 115 | os: [ ubuntu-latest ] 116 | ruby-version: [3.1.2] 117 | 118 | runs-on: ${{ matrix.os }} 119 | 120 | steps: 121 | - uses: actions/checkout@v2 122 | with: 123 | persist-credentials: false 124 | 125 | - name: Set up Ruby ${{ matrix.ruby-version }} 126 | uses: ruby/setup-ruby@v1 127 | with: 128 | ruby-version: ${{ matrix.ruby-version }} 129 | bundler-cache: false 130 | 131 | - name: Install ruby dependencies 132 | run: bundle install 133 | 134 | - name: Zeitwerk Check 135 | run: bundle exec rails zeitwerk:check 136 | -------------------------------------------------------------------------------- /.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 all logfiles and tempfiles. 11 | /log/* 12 | /tmp/* 13 | !/log/.keep 14 | !/tmp/.keep 15 | 16 | # Ignore pidfiles, but keep the directory. 17 | /tmp/pids/* 18 | !/tmp/pids/ 19 | !/tmp/pids/.keep 20 | 21 | # Ignore uploaded files in development. 22 | /storage/* 23 | !/storage/.keep 24 | /tmp/storage/* 25 | !/tmp/storage/ 26 | !/tmp/storage/.keep 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | # Ignore ds_store files. 32 | .DS_Store 33 | 34 | .byebug_history 35 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - rubocop-rails 5 | - rubocop-performance 6 | - rubocop-minitest 7 | 8 | Rails: 9 | Enabled: true 10 | 11 | AllCops: 12 | NewCops: disable 13 | 14 | Metrics/AbcSize: 15 | Max: 31 16 | Exclude: 17 | - 'test/**/*_test.rb' 18 | 19 | Metrics/MethodLength: 20 | Max: 30 21 | Exclude: 22 | - 'test/**/*_test.rb' 23 | 24 | Metrics/BlockLength: 25 | Max: 100 26 | Exclude: 27 | - 'test/**/*_test.rb' 28 | 29 | Metrics/ClassLength: 30 | Exclude: 31 | - 'test/**/*_test.rb' 32 | 33 | Style/Documentation: 34 | Enabled: false 35 | 36 | Style/HashEachMethods: 37 | Enabled: true 38 | 39 | Style/HashTransformKeys: 40 | Enabled: true 41 | 42 | Style/HashTransformValues: 43 | Enabled: true 44 | 45 | Layout/SpaceAroundMethodCallOperator: 46 | Enabled: true 47 | 48 | Lint/RaiseException: 49 | Enabled: true 50 | 51 | Lint/StructNewOverride: 52 | Enabled: true 53 | 54 | Style/ExponentialNotation: 55 | Enabled: true 56 | 57 | Style/FrozenStringLiteralComment: 58 | Enabled: true 59 | 60 | Style/StringLiterals: 61 | Enabled: true 62 | EnforcedStyle: single_quotes 63 | 64 | Style/StringLiteralsInInterpolation: 65 | Enabled: true 66 | EnforcedStyle: single_quotes 67 | 68 | Naming/MemoizedInstanceVariableName: 69 | Exclude: 70 | - 'lib/generators/**/*.rb' -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2022-02-11 17:17:45 UTC using RuboCop version 1.25.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: 6 10 | # Cop supports --auto-correct. 11 | Layout/EmptyLineAfterGuardClause: 12 | Exclude: 13 | - 'bin/bundle' 14 | 15 | # Offense count: 1 16 | # Cop supports --auto-correct. 17 | Layout/EmptyLines: 18 | Exclude: 19 | - 'config/environments/development.rb' 20 | 21 | # Offense count: 1 22 | # Cop supports --auto-correct. 23 | # Configuration parameters: EnforcedStyle. 24 | # SupportedStyles: empty_lines, no_empty_lines 25 | Layout/EmptyLinesAroundBlockBody: 26 | Exclude: 27 | - 'db/schema.rb' 28 | 29 | # Offense count: 1 30 | # Cop supports --auto-correct. 31 | # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment. 32 | Layout/ExtraSpacing: 33 | Exclude: 34 | - 'config/environments/production.rb' 35 | 36 | # Offense count: 1 37 | # Cop supports --auto-correct. 38 | # Configuration parameters: EnforcedStyle, IndentationWidth. 39 | # SupportedStyles: aligned, indented 40 | Layout/MultilineOperationIndentation: 41 | Exclude: 42 | - 'bin/bundle' 43 | 44 | # Offense count: 1 45 | # Cop supports --auto-correct. 46 | # Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator. 47 | # SupportedStylesForExponentOperator: space, no_space 48 | Layout/SpaceAroundOperators: 49 | Exclude: 50 | - 'config/environments/production.rb' 51 | 52 | # Offense count: 2 53 | # Cop supports --auto-correct. 54 | # Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets. 55 | # SupportedStyles: space, no_space, compact 56 | # SupportedStylesForEmptyBrackets: space, no_space 57 | Layout/SpaceInsideArrayLiteralBrackets: 58 | Exclude: 59 | - 'config/environments/production.rb' 60 | 61 | # Offense count: 4 62 | # Cop supports --auto-correct. 63 | Layout/SpaceInsidePercentLiteralDelimiters: 64 | Exclude: 65 | - 'Gemfile' 66 | 67 | # Offense count: 1 68 | # Configuration parameters: IgnoredMethods. 69 | Metrics/CyclomaticComplexity: 70 | Max: 9 71 | 72 | # Offense count: 1 73 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 74 | Metrics/MethodLength: 75 | Max: 13 76 | 77 | # Offense count: 1 78 | # Configuration parameters: IgnoredMethods. 79 | Metrics/PerceivedComplexity: 80 | Max: 9 81 | 82 | # Offense count: 2 83 | # Cop supports --auto-correct. 84 | # Configuration parameters: EnforcedStyle. 85 | # SupportedStyles: nested, compact 86 | Style/ClassAndModuleChildren: 87 | Exclude: 88 | - 'test/channels/application_cable/connection_test.rb' 89 | - 'test/test_helper.rb' 90 | 91 | # Offense count: 1 92 | # Configuration parameters: AllowedConstants. 93 | Style/Documentation: 94 | Exclude: 95 | - 'spec/**/*' 96 | - 'test/**/*' 97 | - 'config/application.rb' 98 | 99 | # Offense count: 1 100 | # Cop supports --auto-correct. 101 | Style/ExpandPathArguments: 102 | Exclude: 103 | - 'bin/bundle' 104 | 105 | # Offense count: 27 106 | # Cop supports --auto-correct. 107 | # Configuration parameters: EnforcedStyle. 108 | # SupportedStyles: always, always_true, never 109 | Style/FrozenStringLiteralComment: 110 | Enabled: false 111 | 112 | # Offense count: 1 113 | # Cop supports --auto-correct. 114 | Style/GlobalStdStream: 115 | Exclude: 116 | - 'config/environments/production.rb' 117 | 118 | # Offense count: 2 119 | # Cop supports --auto-correct. 120 | Style/IfUnlessModifier: 121 | Exclude: 122 | - 'bin/bundle' 123 | 124 | # Offense count: 1 125 | # Cop supports --auto-correct. 126 | Style/PerlBackrefs: 127 | Exclude: 128 | - 'bin/bundle' 129 | 130 | # Offense count: 2 131 | # Cop supports --auto-correct-all. 132 | # Configuration parameters: SafeForConstants. 133 | Style/RedundantFetchBlock: 134 | Exclude: 135 | - 'config/puma.rb' 136 | 137 | # Offense count: 1 138 | # Cop supports --auto-correct. 139 | # Configuration parameters: RequireEnglish. 140 | # SupportedStyles: use_perl_names, use_english_names 141 | Style/SpecialGlobalVars: 142 | EnforcedStyle: use_perl_names 143 | 144 | # Offense count: 79 145 | # Cop supports --auto-correct. 146 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 147 | # SupportedStyles: single_quotes, double_quotes 148 | Style/StringLiterals: 149 | Enabled: false 150 | 151 | # Offense count: 1 152 | # Cop supports --auto-correct. 153 | # Configuration parameters: . 154 | # SupportedStyles: percent, brackets 155 | Style/SymbolArray: 156 | EnforcedStyle: percent 157 | MinSize: 10 158 | 159 | # Offense count: 2 160 | # Cop supports --auto-correct. 161 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 162 | # URISchemes: http, https 163 | Layout/LineLength: 164 | Max: 198 165 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 5 | 6 | ruby '3.1.2' 7 | 8 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 9 | gem 'rails', '~> 7.0', '>= 7.0.4' 10 | 11 | # Use postgresql as the database for Active Record 12 | gem 'pg', '~> 1.1' 13 | 14 | # Use the Puma web server [https://github.com/puma/puma] 15 | gem 'puma', '~> 5.0' 16 | 17 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 18 | gem 'jbuilder' 19 | 20 | # Use Redis adapter to run Action Cable in production 21 | # gem "redis", "~> 4.0" 22 | 23 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 24 | # gem "kredis" 25 | 26 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 27 | # gem "bcrypt", "~> 3.1.7" 28 | 29 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 30 | gem 'tzinfo-data', platforms: %i[ mingw mswin x64_mingw jruby ] 31 | 32 | # Reduces boot times through caching; required in config/boot.rb 33 | gem 'bootsnap', require: false 34 | 35 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 36 | # gem "image_processing", "~> 1.2" 37 | 38 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 39 | gem 'rack-cors' 40 | 41 | group :development, :test do 42 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 43 | gem 'debug', platforms: %i[ mri mingw x64_mingw ] 44 | 45 | # A library for generating fake data [https://github.com/faker-ruby/faker] 46 | gem 'faker', git: 'https://github.com/faker-ruby/faker.git', branch: 'master' 47 | 48 | # Pretty print your Ruby objects [https://github.com/awesome-print/awesome_print] 49 | gem 'awesome_print' 50 | 51 | # A Ruby static code analyzer and formatter, based on the community Ruby style guide [https://github.com/rubocop/rubocop] 52 | gem 'rubocop', require: false 53 | 54 | # An extension of RuboCop focused on code performance checks [https://github.com/rubocop/rubocop-performance] 55 | gem 'rubocop-performance', require: false 56 | 57 | # A RuboCop extension focused on enforcing Rails best practices and coding conventions [https://github.com/rubocop/rubocop-rails] 58 | gem 'rubocop-rails', require: false 59 | 60 | # Code style checking for Minitest files [https://github.com/rubocop/rubocop-minitest] 61 | gem 'rubocop-minitest', require: false 62 | 63 | # Pry is a runtime developer console and IRB alternative with powerful introspection capabilities [https://github.com/pry/pry] 64 | gem 'pry', '~> 0.14.1' 65 | 66 | # Byebug is a Ruby debugger [https://github.com/deivid-rodriguez/byebug] 67 | gem 'byebug', '~> 11.1', '>= 11.1.3' 68 | end 69 | 70 | group :development do 71 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 72 | # gem "spring" 73 | 74 | # E-mail opener [https://github.com/ryanb/letter_opener] 75 | gem 'letter_opener', '~> 1.4', '>= 1.4.1' 76 | end 77 | 78 | group :test do 79 | # Making tests easy on the fingers and eyes [https://github.com/thoughtbot/shoulda] 80 | gem 'shoulda', '~> 4.0' 81 | 82 | # Simple one-liner tests for common Rails functionality [https://github.com/thoughtbot/shoulda-matchers#minitest] 83 | gem 'shoulda-matchers', '~> 4.0' 84 | 85 | # A library for setting up Ruby objects as test data. [https://github.com/thoughtbot/factory_bot_rails] 86 | gem 'factory_bot_rails', '~> 6.2' 87 | 88 | # Mocha is a mocking and stubbing library for Ruby [https://github.com/freerange/mocha] 89 | gem 'mocha', '~> 1.13' 90 | 91 | # Allows you to focus on a few tests with ease without having to use command-line arguments [https://github.com/seattlerb/minitest-focus] 92 | gem 'minitest-focus' 93 | end 94 | 95 | # Define and serve live-updating Swagger JSON for Ruby apps [https://github.com/fotinakis/swagger-blocks] 96 | gem 'swagger-blocks', '~> 3.0' 97 | 98 | # Kaminari is a Scope & Engine based customizable and sophisticated paginator [https://github.com/kaminari/kaminari] 99 | gem 'kaminari', '~> 1.2', '>= 1.2.2' 100 | 101 | # Useful, common monads in idiomatic Ruby [https://github.com/dry-rb/dry-monads] 102 | gem 'dry-monads', '~> 1.4' 103 | 104 | # A simple validation library [https://github.com/dry-rb/dry-validation] 105 | gem 'dry-validation', '~> 1.8', '>= 1.8.1' 106 | 107 | # Typed structs and value objects [https://github.com/dry-rb/dry-struct] 108 | gem 'dry-struct', '~> 1.4' 109 | 110 | # Type system for Ruby supporting coercions, constraints and complex types like structs, value objects, enums etc [https://github.com/dry-rb/dry-types] 111 | gem 'dry-types', '~> 1.5', '>= 1.5.1' 112 | 113 | # DSL for declaring params and options of the initializer [https://github.com/dry-rb/dry-initializer] 114 | gem 'dry-initializer', '~> 3.1', '>= 3.1.1' 115 | 116 | # Flexible authentication solution for Rails with Warden [https://github.com/heartcombo/devise] 117 | gem 'devise', '~> 4.8', '>= 4.8.1' 118 | 119 | # Doorkeeper is an OAuth 2 provider for Rails and Grape [https://github.com/doorkeeper-gem/doorkeeper/] 120 | gem 'doorkeeper', '~> 5.5', '>= 5.5.4' 121 | 122 | # Simple, efficient background processing for Ruby [https://github.com/mperham/sidekiq] 123 | gem 'sidekiq', '~> 6.4', '>= 6.4.1' 124 | 125 | # Object-based searching [https://github.com/activerecord-hackery/ransack] 126 | gem 'ransack', github: 'activerecord-hackery/ransack' 127 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bin/rails server 2 | worker: bundle exec sidekiq -C config/sidekiq.yml 3 | release: bundle exec rails db:migrate -t 4 | -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: bin/rails server -p 3000 2 | worker: bundle exec sidekiq -C config/sidekiq.yml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build & test & quality](https://github.com/cousins-factory/rails-api-boilerplate/actions/workflows/main.yml/badge.svg?branch=main) 2 | [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop) 3 | ![Ruby Version](https://img.shields.io/badge/ruby_version-3.1.2-blue.svg) 4 | ![Rails Version](https://img.shields.io/badge/rails_version-7.0.4-c52f24.svg) 5 | [![Swagger documentation](https://img.shields.io/badge/swagger_documentation-84e92c.svg?&logo=swagger&logoColor=black)](docs/SWAGGER.md) 6 | 7 | # Rails API Boilerplate 8 | ![cover](docs/cover.png) 9 | 10 | # How to Works? 11 | ```mermaid 12 | flowchart TD 13 | R[Request] --> C[Application Controller] 14 | C --> O[Application Operation] 15 | O -- Validate Params --> AC[Application Contract] 16 | O -- Application Contract returned success?--> S[Application Service] 17 | AC -- Validation success? --> O[Application Operation] 18 | AC -- Validation failed? --> E[Contract Errors] 19 | E --> RE 20 | S -- Process successful? --> RS[Resource] 21 | S -- Process failed? --> OE[Resource Errors] 22 | OE --> RE[Response] 23 | RS --> RE 24 | RE -- Returns --> C 25 | ``` 26 | 27 | # Documentations 28 | - [Swagger](docs/SWAGGER.md) 29 | - [Service generator](docs/SERVICE.md) 30 | - [Contract generator](docs/CONTRACT.md) 31 | - [Search & Filter & Sort](docs/RANSACK.md) 32 | 33 | # Installation 34 | ## Prerequisites 35 | - [Ruby](https://rvm.io/) 36 | - [PostgreSQL](https://www.postgresql.org/) 37 | - [Redis](https://redis.io/) 38 | 39 | ## Installation 40 | - Install GEM dependencies: 41 | ```bash 42 | bundle install 43 | ``` 44 | 45 | - Create database, migrate tables and run the seed data: 46 | ```bash 47 | rails db:create 48 | rails db:migrate 49 | rails db:seed 50 | ``` 51 | 52 | - If you are setting up again, when you already have previous databases: 53 | ```bash 54 | rails db:reset 55 | ``` 56 | `reset` is equivalent of `rails db:drop & rails db:setup`. 57 | 58 | - Run the server 59 | ```bash 60 | ./bin/dev 61 | ``` 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require_relative 'config/application' 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Channel < ActionCable::Channel::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationCable 4 | class Connection < ActionCable::Connection::Base 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/contracts/application_contract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-validation' 4 | 5 | class ApplicationContract < Dry::Validation::Contract 6 | config.messages.backend = :i18n 7 | 8 | register_macro(:password_confirmation) do 9 | key.failure(:same_password?) unless values[:password].eql?(values[:password_confirmation]) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/contracts/users/passwords/send_instructions_contract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class SendInstructionsContract < ApplicationContract 6 | params do 7 | required(:email).filled(Types::Email) 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/contracts/users/passwords/update_contract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class UpdateContract < ApplicationContract 6 | params do 7 | required(:reset_password_token).filled(:string) 8 | required(:password) { filled? & str? & min_size?(Devise.password_length.min) } 9 | required(:password_confirmation) { filled? & str? & min_size?(Devise.password_length.min) } 10 | end 11 | 12 | rule(:password).validate(:password_confirmation) 13 | rule(:password_confirmation).validate(:password_confirmation) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/contracts/users/registrations/register_contract.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Registrations 5 | class RegisterContract < ApplicationContract 6 | params do 7 | required(:email).filled(Types::Email) 8 | required(:password).filled(:str?, min_size?: Devise.password_length.min) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::API 4 | include ParameterObjects 5 | end 6 | -------------------------------------------------------------------------------- /app/controllers/authenticated_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AuthenticatedController < ApplicationController 4 | # equivalent of authenticate_user! on devise, but this one will check the oauth token 5 | before_action :doorkeeper_authorize! 6 | 7 | around_action :switch_locale 8 | 9 | private 10 | 11 | # helper method to access the current user from the token 12 | def current_user 13 | @current_user ||= doorkeeper_token.resource_owner 14 | end 15 | 16 | def switch_locale(&action) 17 | locale = current_user.try(:locale) || I18n.default_locale 18 | I18n.with_locale(locale, &action) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/concerns/doorkeeper/authorize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module Authorize 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | before_action :find_doorkeeper_application! 9 | 10 | attr_reader :current_doorkeeper_application 11 | end 12 | 13 | private 14 | 15 | def find_doorkeeper_application! 16 | @current_doorkeeper_application = Doorkeeper::Application.find_by(uid: params[:client_id], 17 | secret: params[:client_secret]) 18 | 19 | render json: invalid_client_response, status: :unauthorized if @current_doorkeeper_application.blank? 20 | end 21 | 22 | def invalid_client_response 23 | { errors: [I18n.t('doorkeeper.errors.messages.invalid_client')] } 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/concerns/parameter_objects.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ParameterObjects 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | def pagination_object 8 | page = params.fetch(:page, 1) 9 | per_page = params.fetch(:per_page, Kaminari.config.default_per_page) 10 | 11 | CustomObjects::ParameterObject::Pagination.new(page:, per_page:) 12 | end 13 | 14 | def query_object 15 | query = params.fetch(:query, {}) 16 | 17 | CustomObjects::ParameterObject::Query.new(query:) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/controllers/swagger_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SwaggerController < ApplicationController 4 | def index 5 | render html: nil, layout: 'layouts/swagger' 6 | end 7 | 8 | def v1_data 9 | swagger_data = SwaggerDocs.v1_swagger_root 10 | 11 | render json: swagger_data, status: :ok 12 | end 13 | 14 | def v2_data 15 | swagger_data = SwaggerDocs.v2_swagger_root 16 | 17 | render json: swagger_data, status: :ok 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/controllers/v1/users/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | module Users 5 | class PasswordsController < ApplicationController 6 | include Doorkeeper::Authorize 7 | 8 | def create 9 | operation = ::Users::Passwords::CreateOperation.new(params: password_params).call 10 | 11 | if operation.success? 12 | render json: operation.success, status: :ok 13 | else 14 | render json: operation.failure, status: :unprocessable_entity 15 | end 16 | end 17 | 18 | def update 19 | operation = ::Users::Passwords::UpdateOperation.new(params: password_params).call 20 | 21 | if operation.success? 22 | render json: operation.success, status: :ok 23 | else 24 | render json: operation.failure, status: :unprocessable_entity 25 | end 26 | end 27 | 28 | private 29 | 30 | def password_params 31 | params.permit(:email, :reset_password_token, :password, :password_confirmation) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/v1/users/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | module Users 5 | class RegistrationsController < ApplicationController 6 | include Doorkeeper::Authorize 7 | 8 | def create 9 | operation = ::Users::Registrations::CreateOperation.new(params: registration_params, 10 | doorkeeper_application: current_doorkeeper_application).call 11 | 12 | if operation.success? 13 | render json: operation.success, status: :created 14 | else 15 | render json: operation.failure, status: :unprocessable_entity 16 | end 17 | end 18 | 19 | private 20 | 21 | def registration_params 22 | params.permit(:email, :password) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/controllers/v1/users/tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module V1 4 | module Users 5 | class TokensController < Doorkeeper::TokensController 6 | private 7 | 8 | def revocation_error_response 9 | error_description = I18n.t('doorkeeper.errors.messages.revoke.unauthorized') 10 | 11 | { errors: [error_description] } 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/app/helpers/.keep -------------------------------------------------------------------------------- /app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationJob < ActiveJob::Base 4 | # Automatically retry jobs that encountered a deadlock 5 | # retry_on ActiveRecord::Deadlocked 6 | 7 | # Most jobs are safe to ignore if the underlying records are no longer available 8 | # discard_on ActiveJob::DeserializationError 9 | end 10 | -------------------------------------------------------------------------------- /app/lib/custom_objects/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-struct' 4 | 5 | module CustomObjects 6 | class Base < Dry::Struct 7 | transform_keys(&:to_sym) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/lib/custom_objects/parameter_object/pagination.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CustomObjects 4 | module ParameterObject 5 | class Pagination < Base 6 | attribute :page, Types::Coercible::Integer.default(1) 7 | attribute :per_page, Types::Coercible::Integer.default(Kaminari.config.default_per_page) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/lib/custom_objects/parameter_object/query.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module CustomObjects 4 | module ParameterObject 5 | class Query < Base 6 | SUPPORTED_CONDITIONS = %w[ 7 | eq not_eq matches does_not_match lt gteq lteq gt in 8 | not_in cont cont_any cont_all not_cont_all i_cont 9 | not_i_cont i_cont_any not_i_cont_any i_cont_all 10 | not_i_cont_all start not_start end not_end true 11 | not_true false not_false present blank null not_null 12 | ].freeze 13 | 14 | DEFAULT = {}.freeze 15 | 16 | attribute :query, Types::Params::Hash.default(DEFAULT) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/lib/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-types' 4 | 5 | module Types 6 | include Dry.Types() 7 | 8 | Email = String.constrained(format: URI::MailTo::EMAIL_REGEXP) 9 | end 10 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationMailer < ActionMailer::Base 4 | default from: 'from@example.com' 5 | layout 'mailer' 6 | end 7 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationRecord < ActiveRecord::Base 4 | primary_abstract_class 5 | end 6 | -------------------------------------------------------------------------------- /app/models/concerns/authenticable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Authenticable 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | # Include default devise modules. Others available are: 8 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable 9 | devise :database_authenticatable, :registerable, 10 | :recoverable, :rememberable, :validatable, 11 | :trackable 12 | end 13 | 14 | module ClassMethods 15 | # rubocop:disable Style/RedundantSelf 16 | def authenticate(email, password) 17 | user = self.find_for_authentication(email:) 18 | user&.valid_password?(password) ? user : nil 19 | end 20 | # rubocop:enable Style/RedundantSelf 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class User < ApplicationRecord 4 | include Authenticable 5 | end 6 | -------------------------------------------------------------------------------- /app/operations/application_operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/monads/all' 4 | require 'dry-initializer' 5 | 6 | class ApplicationOperation 7 | extend Dry::Initializer 8 | 9 | include Dry::Monads 10 | include Dry::Monads::Do 11 | include Supports::ApplicationOperation::Helper 12 | include Supports::ApplicationContract::ErrorParser 13 | end 14 | -------------------------------------------------------------------------------- /app/operations/users/passwords/create_operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class CreateOperation < ApplicationOperation 6 | option :params 7 | option :contract, default: proc { Users::Passwords::SendInstructionsContract.new } 8 | 9 | def call 10 | contract_params = yield validate(contract) 11 | result = yield call_service(contract_params) 12 | 13 | Success(result) 14 | end 15 | 16 | private 17 | 18 | def call_service(contract_params) 19 | Users::Passwords::SendInstructionsService.new(params: contract_params).call 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/operations/users/passwords/update_operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class UpdateOperation < ApplicationOperation 6 | option :params 7 | option :contract, default: proc { Users::Passwords::UpdateContract.new } 8 | 9 | def call 10 | contract_params = yield validate(contract) 11 | result = yield call_service(contract_params) 12 | 13 | Success(result) 14 | end 15 | 16 | private 17 | 18 | def call_service(contract_params) 19 | Users::Passwords::UpdateService.new(params: contract_params).call 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/operations/users/registrations/create_operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Registrations 5 | class CreateOperation < ApplicationOperation 6 | option :params 7 | option :doorkeeper_application, type: Types.Instance(Doorkeeper::Application) 8 | option :contract, default: proc { Users::Registrations::RegisterContract.new } 9 | 10 | def call 11 | contract_params = yield validate(contract) 12 | user = yield call_service(contract_params) 13 | 14 | Success(user) 15 | end 16 | 17 | private 18 | 19 | def call_service(contract_params) 20 | Users::Registrations::RegisterService.new(params: contract_params, doorkeeper_application:).call 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/services/application_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/monads/all' 4 | require 'dry-initializer' 5 | 6 | class ApplicationService 7 | extend Dry::Initializer 8 | 9 | include Dry::Monads 10 | include Dry::Monads::Do 11 | include Supports::Sidekiq::Helper 12 | include Supports::ApplicationService::Helper 13 | end 14 | -------------------------------------------------------------------------------- /app/services/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/app/services/concerns/.keep -------------------------------------------------------------------------------- /app/services/doorkeeper/access_tokens/create_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doorkeeper 4 | module AccessTokens 5 | class CreateService < ApplicationService 6 | option :doorkeeper_application, type: Types.Instance(Doorkeeper::Application) 7 | option :user, type: Types.Instance(User) 8 | 9 | def call 10 | access_token = yield create_resource(Doorkeeper::AccessToken) 11 | 12 | Success(access_token) 13 | end 14 | 15 | private 16 | 17 | def params 18 | { 19 | resource_owner_id: user.id, 20 | resource_owner_type: user.class.name, 21 | application_id: doorkeeper_application.id, 22 | refresh_token: generate_refresh_token, 23 | expires_in: Doorkeeper.configuration.access_token_expires_in.to_i 24 | } 25 | end 26 | 27 | def generate_refresh_token 28 | loop do 29 | token = SecureRandom.hex(32) 30 | break token unless Doorkeeper::AccessToken.exists?(refresh_token: token) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/services/users/create_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | class CreateService < ApplicationService 5 | option :params, type: Types::Hash 6 | 7 | def call 8 | user = yield create_resource(User) 9 | 10 | Success(user) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/services/users/passwords/send_instructions_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class SendInstructionsService < ApplicationService 6 | option :params, type: Types::Hash 7 | 8 | def call 9 | user = yield send_password_instructions 10 | result = yield check_result(user) 11 | 12 | Success(result) 13 | end 14 | 15 | private 16 | 17 | def send_password_instructions 18 | user = User.send_reset_password_instructions(params) 19 | 20 | Success(user) 21 | end 22 | 23 | def check_result(user) 24 | return Success(success_message) if successfully_sent?(user) 25 | 26 | resource_failure(user) 27 | end 28 | 29 | def successfully_sent?(user) 30 | Devise.paranoid || user.errors.empty? 31 | end 32 | 33 | def success_message 34 | { message: I18n.t('devise.passwords.send_instructions') } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/services/users/passwords/update_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Passwords 5 | class UpdateService < ApplicationService 6 | option :params, type: Types::Hash 7 | 8 | def call 9 | user = yield update_password 10 | result = yield check_result(user) 11 | 12 | Success(result) 13 | end 14 | 15 | private 16 | 17 | def update_password 18 | user = User.reset_password_by_token(params) 19 | 20 | Success(user) 21 | end 22 | 23 | def check_result(user) 24 | return Success(success_message) if successfully_updated?(user) 25 | 26 | resource_failure(user) 27 | end 28 | 29 | def successfully_updated?(user) 30 | user.errors.empty? 31 | end 32 | 33 | def success_message 34 | { message: I18n.t('devise.passwords.updated_not_active') } 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/services/users/registrations/register_service.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | module Registrations 5 | class RegisterService < ApplicationService 6 | include Supports::Doorkeeper::CustomRegisterResponse 7 | 8 | option :params, type: Types::Hash 9 | option :doorkeeper_application, type: Types.Instance(Doorkeeper::Application) 10 | 11 | def call 12 | ActiveRecord::Base.transaction(requires_new: true) do 13 | user = yield create_user 14 | access_token = yield create_access_token(user) 15 | response = body(user, access_token) 16 | 17 | Success(response) 18 | end 19 | end 20 | 21 | private 22 | 23 | def create_user 24 | Users::CreateService.new(params:).call 25 | end 26 | 27 | def create_access_token(user) 28 | Doorkeeper::AccessTokens::CreateService.new(user:, doorkeeper_application:).call 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/swagger_docs/controllers/v1/users/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Controllers 4 | module V1 5 | module Users 6 | class PasswordsController 7 | include Swagger::Blocks 8 | 9 | swagger_path '/v1/users/password' do 10 | operation :post do 11 | key :summary, 'Reset password' 12 | key :description, 'Send an email about resetting password' 13 | key :operationId, 'sendResetPasswordInformation' 14 | key :tags, [ 15 | 'User Passwords' 16 | ] 17 | 18 | request_body do 19 | key :description, 'User email' 20 | key :required, true 21 | content :'application/json' do 22 | schema do 23 | key :'$ref', :UserResetPasswordInput 24 | end 25 | end 26 | end 27 | 28 | response 200 do 29 | key :description, 'Successful response' 30 | content :'application/json' do 31 | schema do 32 | key :'$ref', :UserResetPasswordSuccessResponse 33 | end 34 | end 35 | end 36 | 37 | response 422 do 38 | key :description, 'Somethins goes wrong' 39 | content :'application/json' do 40 | schema do 41 | key :'$ref', :ErrorResponse 42 | end 43 | end 44 | end 45 | 46 | response 401 do 47 | key :description, 'Invalid client credentials passed' 48 | content :'application/json' do 49 | schema do 50 | key :'$ref', :ErrorResponse 51 | end 52 | end 53 | end 54 | end 55 | 56 | operation :patch do 57 | key :summary, 'Update password' 58 | key :description, 'Update user password with token' 59 | key :operationId, 'updateUserPassword' 60 | key :tags, [ 61 | 'User Passwords' 62 | ] 63 | 64 | request_body do 65 | key :description, 'Password credentials' 66 | key :required, true 67 | content :'application/json' do 68 | schema do 69 | key :'$ref', :UserUpdatePasswordInput 70 | end 71 | end 72 | end 73 | 74 | response 200 do 75 | key :description, 'Successful response' 76 | content :'application/json' do 77 | schema do 78 | key :'$ref', :UserUpdatePasswordSuccessResponse 79 | end 80 | end 81 | end 82 | 83 | response 422 do 84 | key :description, 'Something goes wrong' 85 | content :'application/json' do 86 | schema do 87 | key :'$ref', :ErrorResponse 88 | end 89 | end 90 | end 91 | 92 | response 401 do 93 | key :description, 'Invalid client credentials passed' 94 | content :'application/json' do 95 | schema do 96 | key :'$ref', :ErrorResponse 97 | end 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /app/swagger_docs/controllers/v1/users/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Controllers 4 | module V1 5 | module Users 6 | class RegistrationsController 7 | include Swagger::Blocks 8 | 9 | swagger_path '/v1/users/sign_up' do 10 | operation :post do 11 | key :summary, 'Sign up' 12 | key :description, 'Create a new user and generate access and refresh tokens' 13 | key :operationId, 'userSignUp' 14 | key :tags, [ 15 | 'User Registrations' 16 | ] 17 | 18 | request_body do 19 | key :description, 'User credentials' 20 | key :required, true 21 | content :'application/json' do 22 | schema do 23 | key :'$ref', :UserSignUpInput 24 | end 25 | end 26 | end 27 | 28 | response 201 do 29 | key :description, 'Successful response' 30 | content :'application/json' do 31 | schema do 32 | key :'$ref', :UserSignUpSuccessResponse 33 | end 34 | end 35 | end 36 | 37 | response 422 do 38 | key :description, 'Something goes wrong' 39 | content :'application/json' do 40 | schema do 41 | key :'$ref', :ErrorResponse 42 | end 43 | end 44 | end 45 | 46 | response 401 do 47 | key :description, 'Invalid client credentials passed' 48 | content :'application/json' do 49 | schema do 50 | key :'$ref', :ErrorResponse 51 | end 52 | end 53 | end 54 | end 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/swagger_docs/controllers/v1/users/tokens_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Controllers 4 | module V1 5 | module Users 6 | class TokensController 7 | include Swagger::Blocks 8 | 9 | swagger_path '/v1/users/oauth/token' do 10 | operation :post do 11 | key :summary, 'Generate access and refresh tokens for authentication' 12 | key :description, 'Generate access and refresh tokens for authentication' 13 | key :operationId, 'userSignIn' 14 | key :tags, [ 15 | 'User Sessions' 16 | ] 17 | 18 | request_body do 19 | key :description, 'User credentials' 20 | key :required, true 21 | content :'application/json' do 22 | schema do 23 | key :'$ref', :UserSignInInput 24 | end 25 | end 26 | end 27 | 28 | response 200 do 29 | key :description, 'Successful response' 30 | content :'application/json' do 31 | schema do 32 | key :'$ref', :UserSignInSuccessResponse 33 | end 34 | end 35 | end 36 | 37 | response 400 do 38 | key :description, 'Invalid authentication credentials passed' 39 | content :'application/json' do 40 | schema do 41 | key :'$ref', :ErrorResponse 42 | end 43 | end 44 | end 45 | 46 | response 401 do 47 | key :description, 'Invalid client credentials passed' 48 | content :'application/json' do 49 | schema do 50 | key :'$ref', :ErrorResponse 51 | end 52 | end 53 | end 54 | end 55 | end 56 | 57 | swagger_path '/v1/users/oauth/revoke' do 58 | operation :post do 59 | key :summary, 'Revoke access or refresh tokens' 60 | key :description, 'Revoke access or refresh token' 61 | key :operationId, 'userLogout' 62 | key :tags, [ 63 | 'User Sessions' 64 | ] 65 | 66 | request_body do 67 | key :description, 'Token credentials' 68 | key :required, true 69 | content :'application/json' do 70 | schema do 71 | key :'$ref', :UserRevokeInput 72 | end 73 | end 74 | end 75 | 76 | response 200 do 77 | key :description, 'Default response' 78 | content :'application/json' do 79 | schema do 80 | key :type, :object 81 | end 82 | end 83 | end 84 | 85 | response 403 do 86 | key :description, 'Invalid client credentials passed' 87 | content :'application/json' do 88 | schema do 89 | key :'$ref', :ErrorResponse 90 | end 91 | end 92 | end 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /app/swagger_docs/inputs/v1/user/reset_password_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inputs 4 | module V1 5 | module User 6 | class ResetPasswordInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserResetPasswordInput do 11 | key :required, %i[email client_id client_secret] 12 | 13 | property :email do 14 | key :type, :string 15 | key :example, 'test@test.com' 16 | end 17 | 18 | property :client_id do 19 | key :type, :string 20 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 21 | end 22 | 23 | property :client_secret do 24 | key :type, :string 25 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/swagger_docs/inputs/v1/user/revoke_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inputs 4 | module V1 5 | module User 6 | class RevokeInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserRevokeInput do 11 | key :required, %i[token client_id client_secret] 12 | 13 | property :token do 14 | key :type, :string 15 | key :example, '1qaz2wsx3edc' 16 | end 17 | 18 | property :client_id do 19 | key :type, :string 20 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 21 | end 22 | 23 | property :client_secret do 24 | key :type, :string 25 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/swagger_docs/inputs/v1/user/sign_in_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inputs 4 | module V1 5 | module User 6 | class SignInInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserSignInInput do 11 | key :required, %i[grant_type email password client_id client_secret] 12 | 13 | property :grant_type do 14 | key :type, :string 15 | key :example, 'password' 16 | end 17 | 18 | property :email do 19 | key :type, :string 20 | key :example, 'test@test.com' 21 | end 22 | 23 | property :password do 24 | key :type, :string 25 | key :example, 'password' 26 | end 27 | 28 | property :client_id do 29 | key :type, :string 30 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 31 | end 32 | 33 | property :client_secret do 34 | key :type, :string 35 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/swagger_docs/inputs/v1/user/sign_up_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inputs 4 | module V1 5 | module User 6 | class SignUpInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserSignUpInput do 11 | key :required, %i[email password client_id client_secret] 12 | 13 | property :email do 14 | key :type, :string 15 | key :example, 'test@test.com' 16 | end 17 | 18 | property :password do 19 | key :type, :string 20 | key :example, 'password' 21 | end 22 | 23 | property :client_id do 24 | key :type, :string 25 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 26 | end 27 | 28 | property :client_secret do 29 | key :type, :string 30 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /app/swagger_docs/inputs/v1/user/update_password_input.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Inputs 4 | module V1 5 | module User 6 | class UpdatePasswordInput 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserUpdatePasswordInput do 11 | key :required, %i[reset_password_token password password_confirmation client_id client_secret] 12 | 13 | property :reset_password_token do 14 | key :type, :string 15 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 16 | end 17 | 18 | property :password do 19 | key :type, :string 20 | key :example, 'newpassword' 21 | end 22 | 23 | property :password_confirmation do 24 | key :type, :string 25 | key :example, 'newpassword' 26 | end 27 | 28 | property :client_id do 29 | key :type, :string 30 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 31 | end 32 | 33 | property :client_secret do 34 | key :type, :string 35 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/swagger_docs/models/shared/meta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Models 4 | module Shared 5 | class Meta 6 | include Swagger::Blocks 7 | 8 | swagger_component do 9 | schema :Meta do 10 | key :type, :object 11 | key :required, %i[pagination] 12 | 13 | property :pagination do 14 | key :'$ref', :Pagination 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/swagger_docs/models/shared/pagination.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Models 4 | module Shared 5 | class Pagination 6 | include Swagger::Blocks 7 | 8 | swagger_component do 9 | schema :Pagination do 10 | key :type, :object 11 | key :required, %i[current limit total_pages total_count] 12 | 13 | property :current do 14 | key :type, :integer 15 | key :format, :int32 16 | end 17 | 18 | property :previous do 19 | key :type, :integer 20 | key :format, :int32 21 | end 22 | 23 | property :next do 24 | key :type, :integer 25 | key :format, :int32 26 | end 27 | 28 | property :limit do 29 | key :type, :integer 30 | key :format, :int32 31 | end 32 | 33 | property :total_pages do 34 | key :type, :integer 35 | key :format, :int32 36 | end 37 | 38 | property :total_count do 39 | key :type, :integer 40 | key :format, :int64 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/swagger_docs/parameters/shared/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/app/swagger_docs/parameters/shared/.keep -------------------------------------------------------------------------------- /app/swagger_docs/responses/shared/error_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Responses 4 | module Shared 5 | class ErrorResponse 6 | include Swagger::Blocks 7 | 8 | swagger_component do 9 | schema :ErrorResponse do 10 | key :type, :object 11 | 12 | property :errors do 13 | key :type, :array 14 | items do 15 | key :type, :string 16 | key :example, 'Something goes wrong while executing request' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/swagger_docs/responses/v1/user/reset_password_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Responses 4 | module V1 5 | module User 6 | class ResetPasswordResponse 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserResetPasswordSuccessResponse do 11 | key :type, :object 12 | key :required, %i[message] 13 | 14 | property :message do 15 | key :type, :string 16 | key :example, 'You will receive an email with instructions on how to reset your password in a few minutes.' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/swagger_docs/responses/v1/user/sign_in_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Responses 4 | module V1 5 | module User 6 | class SignInResponse 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserSignInSuccessResponse do 11 | key :type, :object 12 | key :required, %i[access_token token_type expires_in refresh_token created_at user] 13 | 14 | property :access_token do 15 | key :type, :string 16 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 17 | end 18 | 19 | property :token_type do 20 | key :type, :string 21 | key :example, 'Bearer' 22 | end 23 | 24 | property :expires_in do 25 | key :type, :integer 26 | key :example, 7200 27 | end 28 | 29 | property :refresh_token do 30 | key :type, :string 31 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 32 | end 33 | 34 | property :created_at do 35 | key :type, :integer 36 | key :example, 1_661_719_659 37 | end 38 | 39 | property :user do 40 | key :type, :object 41 | key :required, %i[id type email] 42 | 43 | property :id do 44 | key :type, :string 45 | key :format, :uuid 46 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 47 | end 48 | 49 | property :type do 50 | key :type, :string 51 | key :example, 'User' 52 | end 53 | 54 | property :email do 55 | key :type, :string 56 | key :example, 'test@test.com' 57 | end 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/swagger_docs/responses/v1/user/sign_up_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Responses 4 | module V1 5 | module User 6 | class SignUpResponse 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserSignUpSuccessResponse do 11 | key :type, :object 12 | key :required, %i[user access_token token_type expires_in refresh_token created_at] 13 | 14 | property :user do 15 | key :type, :object 16 | key :required, %i[id type email] 17 | 18 | property :id do 19 | key :type, :string 20 | key :format, :uuid 21 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 22 | end 23 | 24 | property :type do 25 | key :type, :string 26 | key :example, 'User' 27 | end 28 | 29 | property :email do 30 | key :type, :string 31 | key :example, 'test@test.com' 32 | end 33 | end 34 | 35 | property :access_token do 36 | key :type, :string 37 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 38 | end 39 | 40 | property :token_type do 41 | key :type, :string 42 | key :example, 'Bearer' 43 | end 44 | 45 | property :expires_in do 46 | key :type, :integer 47 | key :example, 7200 48 | end 49 | 50 | property :refresh_token do 51 | key :type, :string 52 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 53 | end 54 | 55 | property :created_at do 56 | key :type, :integer 57 | key :example, 1_661_719_307 58 | end 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /app/swagger_docs/responses/v1/user/update_password_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Responses 4 | module V1 5 | module User 6 | class UpdatePasswordResponse 7 | include Swagger::Blocks 8 | 9 | swagger_component do 10 | schema :UserUpdatePasswordSuccessResponse do 11 | key :type, :object 12 | key :required, %i[message] 13 | 14 | property :message do 15 | key :type, :string 16 | key :example, 'Your password has been changed successfully.' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/swagger_docs/swagger_docs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SwaggerDocs 4 | include Swagger::Blocks 5 | 6 | COMMON_SWAGGERED_CLASSES = [ 7 | ## Models ## 8 | Models::Shared::Pagination, 9 | Models::Shared::Meta, 10 | ## Responses ## 11 | Responses::Shared::ErrorResponse 12 | ].freeze 13 | 14 | V1_SWAGGERED_CLASSES = [ 15 | ## Controllers ## 16 | Controllers::V1::Users::TokensController, 17 | Controllers::V1::Users::RegistrationsController, 18 | Controllers::V1::Users::PasswordsController, 19 | ## Inputs ## 20 | Inputs::V1::User::SignInInput, 21 | Inputs::V1::User::SignUpInput, 22 | Inputs::V1::User::ResetPasswordInput, 23 | Inputs::V1::User::UpdatePasswordInput, 24 | Inputs::V1::User::RevokeInput, 25 | ## Responses ## 26 | Responses::V1::User::SignInResponse, 27 | Responses::V1::User::SignUpResponse, 28 | Responses::V1::User::ResetPasswordResponse, 29 | Responses::V1::User::UpdatePasswordResponse, 30 | ## Models ## 31 | self 32 | ].concat(COMMON_SWAGGERED_CLASSES) 33 | 34 | V2_SWAGGERED_CLASSES = [ 35 | self 36 | ].concat(COMMON_SWAGGERED_CLASSES) 37 | 38 | swagger_root do 39 | key :openapi, '3.0.0' 40 | 41 | info do 42 | key :version, '1.0.0' 43 | key :title, 'Rails API Boilerplate' 44 | key :description, 'Rails API Boilerplate' 45 | 46 | contact do 47 | key :name, 'SHFT' 48 | key :url, 'https://shft.co' 49 | key :email, 'contact@shft.co' 50 | end 51 | end 52 | 53 | server do 54 | url_options = Rails.application.routes.default_url_options 55 | key :url, "#{url_options[:protocol]}://#{url_options[:host]}" 56 | key :description, 'Rails API Boilerplate' 57 | end 58 | end 59 | 60 | class << self 61 | def v1_swagger_root 62 | build_swagger_root(V1_SWAGGERED_CLASSES) 63 | end 64 | 65 | def v2_swagger_root 66 | build_swagger_root(V2_SWAGGERED_CLASSES) 67 | end 68 | 69 | private 70 | 71 | def build_swagger_root(classes) 72 | swagger_data = Swagger::Blocks.build_root_json(classes) 73 | 74 | swagger_data[:components][:securitySchemes] = { 75 | bearerAuth: { 76 | type: 'http', 77 | scheme: 'bearer', 78 | bearerFormat: 'JWT' 79 | } 80 | } 81 | 82 | swagger_data[:security] = [{ bearerAuth: [] }] 83 | 84 | swagger_data 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /app/views/devise/mailer/confirmation_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome <%= @email %>!

2 | 3 |

You can confirm your account email through the link below:

4 | 5 | '>Confirm my account 6 | -------------------------------------------------------------------------------- /app/views/devise/mailer/email_changed.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @email %>!

2 | 3 | <% if @resource.try(:unconfirmed_email?) %> 4 |

We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.

5 | <% else %> 6 |

We're contacting you to notify you that your email has been changed to <%= @resource.email %>.

7 | <% end %> 8 | -------------------------------------------------------------------------------- /app/views/devise/mailer/password_change.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

We're contacting you to notify you that your password has been changed.

4 | -------------------------------------------------------------------------------- /app/views/devise/mailer/reset_password_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

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

4 | 5 | '>Change my password 6 | 7 |

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

8 |

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

9 | -------------------------------------------------------------------------------- /app/views/devise/mailer/unlock_instructions.html.erb: -------------------------------------------------------------------------------- 1 |

Hello <%= @resource.email %>!

2 | 3 |

Your account has been locked due to an excessive number of unsuccessful sign in attempts.

4 | 5 |

Click the link below to unlock your account:

6 | 7 | '>Unlock my account 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/layouts/swagger.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Swagger UI 5 | 6 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 68 | 69 | -------------------------------------------------------------------------------- /app/views/shared/partials/_meta.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.meta do 4 | json.partial! 'shared/partials/pagination', locals: { collection: } 5 | end 6 | -------------------------------------------------------------------------------- /app/views/shared/partials/_pagination.json.jbuilder: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | json.pagination do 4 | json.current collection.current_page 5 | json.previous collection.prev_page 6 | json.next collection.next_page 7 | json.limit collection.limit_value 8 | json.total_pages collection.total_pages 9 | json.total_count collection.total_count 10 | end 11 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../../Gemfile", __FILE__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile) 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || cli_arg_version || 66 | bundler_requirement_for(lockfile_version) 67 | end 68 | 69 | def bundler_requirement_for(version) 70 | return "#{Gem::Requirement.default}.a" unless version 71 | 72 | bundler_gem_version = Gem::Version.new(version) 73 | 74 | requirement = bundler_gem_version.approximate_recommendation 75 | 76 | return requirement unless Gem.rubygems_version < Gem::Version.new("2.7.0") 77 | 78 | requirement += ".a" if bundler_gem_version.prerelease? 79 | 80 | requirement 81 | end 82 | 83 | def load_bundler! 84 | ENV["BUNDLE_GEMFILE"] ||= gemfile 85 | 86 | activate_bundler 87 | end 88 | 89 | def activate_bundler 90 | gem_error = activation_error_handling do 91 | gem "bundler", bundler_requirement 92 | end 93 | return if gem_error.nil? 94 | require_error = activation_error_handling do 95 | require "bundler/version" 96 | end 97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 99 | exit 42 100 | end 101 | 102 | def activation_error_handling 103 | yield 104 | nil 105 | rescue StandardError, LoadError => e 106 | e 107 | end 108 | end 109 | 110 | m.load_bundler! 111 | 112 | if m.invoked_as_script? 113 | load Gem.bin_path("bundler", "bundle") 114 | end 115 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! command -v foreman &> /dev/null 4 | then 5 | echo "Installing foreman..." 6 | gem install foreman 7 | fi 8 | 9 | foreman start -p 3000 -f Procfile.dev "$@" 10 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /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 set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time 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 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require_relative 'config/environment' 6 | 7 | run Rails.application 8 | Rails.application.load_server 9 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'boot' 4 | 5 | require 'rails/all' 6 | 7 | # Require the gems listed in Gemfile, including any gems 8 | # you've limited to :test, :development, or :production. 9 | Bundler.require(*Rails.groups) 10 | 11 | module RailsApiBoilerplate 12 | class Application < Rails::Application 13 | # Initialize configuration defaults for originally generated Rails version. 14 | config.load_defaults 7.0 15 | 16 | I18n.load_path += Dir[Rails.root.join('config/locales/**/*.yml').to_s] 17 | I18n.available_locales = %i[en tr] 18 | 19 | config.i18n.default_locale = :en 20 | 21 | # Configuration for the application, engines, and railties goes here. 22 | # 23 | # These settings can be overridden in specific environments using the files 24 | # in config/environments, which are processed later. 25 | # 26 | # config.time_zone = "Central Time (US & Canada)" 27 | # config.eager_load_paths << Rails.root.join("extras") 28 | 29 | # Require supports 30 | Dir[Rails.root.join('lib/supports/**/*.rb')].each { |file| require file } 31 | 32 | config.generators do |g| 33 | g.test_framework :test_unit, fixture: false 34 | g.orm :active_record, primary_key_type: :uuid 35 | end 36 | 37 | # Only loads a smaller set of middleware suitable for API only apps. 38 | # Middleware like session, flash, cookies can be added back manually. 39 | # Skip views, helpers and assets when generating a new resource. 40 | config.api_only = true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 4 | 5 | require 'bundler/setup' # Set up gems listed in the Gemfile. 6 | require 'bootsnap/setup' # Speed up boot time by caching expensive operations. 7 | -------------------------------------------------------------------------------- /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 | lqNhiRbjsLdDi8GtVcTjsJpN7zm52vq0Uvwt1Rch95CKVoywdGXoiVRv3qSqL6IFxtaRYpURuaoseqMDvgc5z5lKEQeOY2kr4gPpxBwRQBPV1aV0zfTBtxdcLDg3MAdnzQ9JEdMkMIXejJcEbjB+CWRa2RcplfMFziNUzkg1X4T5vASjK7c0efnVLQsskHVDqCJhWBNyrnUUi9yO7ZFPoNJQ3AB5UCgoI2+uiDajorL8yu6HauDpwUVGSVi5R4XLoMMRpNJnRPQOW1U2Dlrs4pFY2urEjfboEuZOuBLeDUvkzkHkiAGMFfEEZIPGypOvCi2paROtW10nUPGUurdT+UNnw2I4Ahxpp46xU0pOVFyJbxFH6SBdf7Pttkp824LRDemnyPPoGZHusjvX4CFdCaeVuiIq6zikAH14--5nFC+VevtiVbgT+m--keYGK6wSKq9SUMc8eAE4iw== -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.3 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On macOS with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On macOS with MacPorts: 8 | # gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config 9 | # On Windows: 10 | # gem install pg 11 | # Choose the win32 build. 12 | # Install PostgreSQL and put its /bin directory on your path. 13 | # 14 | # Configure Using Gemfile 15 | # gem "pg" 16 | # 17 | default: &default 18 | adapter: postgresql 19 | encoding: unicode 20 | # For details on connection pooling, see Rails configuration guide 21 | # https://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: rails_api_boilerplate_development 27 | 28 | # The specified database role being used to connect to postgres. 29 | # To create additional roles in postgres see `$ createuser --help`. 30 | # When left blank, postgres will use the default role. This is 31 | # the same name as the operating system user running Rails. 32 | #username: rails_api_boilerplate 33 | 34 | # The password associated with the postgres role (username). 35 | #password: 36 | 37 | # Connect on a TCP socket. Omitted by default since the client uses a 38 | # domain socket that doesn't need configuration. Windows does not have 39 | # domain sockets, so uncomment these lines. 40 | #host: localhost 41 | 42 | # The TCP port the server listens on. Defaults to 5432. 43 | # If your server runs on a different port number, change accordingly. 44 | #port: 5432 45 | 46 | # Schema search path. The server defaults to $user,public 47 | #schema_search_path: myapp,sharedapp,public 48 | 49 | # Minimum log levels, in increasing order: 50 | # debug5, debug4, debug3, debug2, debug1, 51 | # log, notice, warning, error, fatal, and panic 52 | # Defaults to warning. 53 | #min_messages: notice 54 | 55 | # Warning: The database defined as "test" will be erased and 56 | # re-generated from your development database when you run "rake". 57 | # Do not set this db to the same as development or production. 58 | test: 59 | <<: *default 60 | database: rails_api_boilerplate_test 61 | host: localhost 62 | # ENV POSTGRES_USER and POSTGRES_PASSWORD set in CI, usually empty 63 | # in local tests, where pg is usually running with no authentication required 64 | username: <%= ENV['POSTGRES_USER'] %> 65 | password: <%= ENV['POSTGRES_PASSWORD'] %> 66 | 67 | # As with config/credentials.yml, you never want to store sensitive information, 68 | # like your database password, in your source code. If your source code is 69 | # ever seen by anyone, they now have access to your database. 70 | # 71 | # Instead, provide the password or a full connection URL as an environment 72 | # variable when you boot the app. For example: 73 | # 74 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 75 | # 76 | # If the connection URL is provided in the special DATABASE_URL environment 77 | # variable, Rails will automatically merge its configuration values on top of 78 | # the values provided in this file. Alternatively, you can specify a connection 79 | # URL environment variable explicitly: 80 | # 81 | # production: 82 | # url: <%= ENV["MY_APP_DATABASE_URL"] %> 83 | # 84 | # Read https://guides.rubyonrails.org/configuring.html#configuring-a-database 85 | # for a full overview on how database connection configuration can be specified. 86 | # 87 | production: 88 | <<: *default 89 | database: rails_api_boilerplate_production 90 | username: rails_api_boilerplate 91 | password: <%= ENV["RAILS_API_BOILERPLATE_DATABASE_PASSWORD"] %> 92 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require_relative 'application' 5 | 6 | # Initialize the Rails application. 7 | Rails.application.initialize! 8 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | APP_HOST = "lvh.me:#{ENV.fetch('PORT', 3000)}".freeze 6 | URL_PROTOCOL = ENV['URL_PROTOCOL'] || 'http' 7 | 8 | Rails.application.routes.default_url_options[:host] = APP_HOST 9 | Rails.application.routes.default_url_options[:protocol] = URL_PROTOCOL 10 | 11 | Rails.application.configure do 12 | # Settings specified here will take precedence over those in config/application.rb. 13 | 14 | config.hosts << '.lvh.me' 15 | 16 | # In the development environment your application's code is reloaded any time 17 | # it changes. This slows down response time but is perfect for development 18 | # since you don't have to restart the web server when you make code changes. 19 | config.cache_classes = false 20 | 21 | # Do not eager load code on boot. 22 | config.eager_load = false 23 | 24 | # Show full error reports. 25 | config.consider_all_requests_local = true 26 | 27 | # Enable server timing 28 | config.server_timing = true 29 | 30 | # Enable/disable caching. By default caching is disabled. 31 | # Run rails dev:cache to toggle caching. 32 | if Rails.root.join('tmp/caching-dev.txt').exist? 33 | config.cache_store = :memory_store 34 | config.public_file_server.headers = { 35 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 36 | } 37 | else 38 | config.action_controller.perform_caching = false 39 | 40 | config.cache_store = :null_store 41 | end 42 | 43 | # Store uploaded files on the local file system (see config/storage.yml for options). 44 | config.active_storage.service = :local 45 | 46 | # Don't care if the mailer can't send. 47 | config.action_mailer.raise_delivery_errors = false 48 | 49 | config.action_mailer.perform_caching = false 50 | 51 | # letter opener 52 | config.action_mailer.delivery_method = :letter_opener 53 | 54 | config.action_mailer.perform_deliveries = true 55 | 56 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } 57 | 58 | # Print deprecation notices to the Rails logger. 59 | config.active_support.deprecation = :log 60 | 61 | # Raise exceptions for disallowed deprecations. 62 | config.active_support.disallowed_deprecation = :raise 63 | 64 | # Tell Active Support which deprecation messages to disallow. 65 | config.active_support.disallowed_deprecation_warnings = [] 66 | 67 | # Raise an error on page load if there are pending migrations. 68 | config.active_record.migration_error = :page_load 69 | 70 | # Highlight code that triggered database queries in logs. 71 | config.active_record.verbose_query_logs = true 72 | 73 | 74 | # Raises error for missing translations. 75 | # config.i18n.raise_on_missing_translations = true 76 | 77 | # Annotate rendered view with file names. 78 | # config.action_view.annotate_rendered_view_with_filenames = true 79 | 80 | # Uncomment if you wish to allow Action Cable access from any origin. 81 | # config.action_cable.disable_request_forgery_protection = true 82 | end 83 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | Rails.application.configure do 6 | # Settings specified here will take precedence over those in config/application.rb. 7 | 8 | # Code is not reloaded between requests. 9 | config.cache_classes = true 10 | 11 | # Eager load code on boot. This eager loads most of Rails and 12 | # your application in memory, allowing both threaded web servers 13 | # and those relying on copy on write to perform better. 14 | # Rake tasks automatically ignore this option for performance. 15 | config.eager_load = true 16 | 17 | # Full error reports are disabled and caching is turned on. 18 | config.consider_all_requests_local = false 19 | 20 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 21 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 22 | # config.require_master_key = true 23 | 24 | # Disable serving static files from the `/public` folder by default since 25 | # Apache or NGINX already handles this. 26 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 27 | 28 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 29 | # config.asset_host = "http://assets.example.com" 30 | 31 | # Specifies the header that your server uses for sending files. 32 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 33 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 34 | 35 | # Store uploaded files on the local file system (see config/storage.yml for options). 36 | config.active_storage.service = :local 37 | 38 | # Mount Action Cable outside main process or domain. 39 | # config.action_cable.mount_path = nil 40 | # config.action_cable.url = "wss://example.com/cable" 41 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] 42 | 43 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 44 | # config.force_ssl = true 45 | 46 | # Include generic and useful information about system operation, but avoid logging too much 47 | # information to avoid inadvertent exposure of personally identifiable information (PII). 48 | config.log_level = :info 49 | 50 | # Prepend all log lines with the following tags. 51 | config.log_tags = [ :request_id ] 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Use a real queuing backend for Active Job (and separate queues per environment). 57 | # config.active_job.queue_adapter = :resque 58 | # config.active_job.queue_name_prefix = "rails_api_boilerplate_production" 59 | 60 | config.action_mailer.perform_caching = false 61 | 62 | # Ignore bad email addresses and do not raise email delivery errors. 63 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 64 | # config.action_mailer.raise_delivery_errors = false 65 | 66 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 67 | # the I18n.default_locale when a translation cannot be found). 68 | config.i18n.fallbacks = true 69 | 70 | # Don't log any deprecations. 71 | config.active_support.report_deprecations = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Use a different logger for distributed setups. 77 | # require "syslog/logger" 78 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 79 | 80 | if ENV['RAILS_LOG_TO_STDOUT'].present? 81 | logger = ActiveSupport::Logger.new(STDOUT) 82 | logger.formatter = config.log_formatter 83 | config.logger = ActiveSupport::TaggedLogging.new(logger) 84 | end 85 | 86 | # Do not dump schema after migrations. 87 | config.active_record.dump_schema_after_migration = false 88 | end 89 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/core_ext/integer/time' 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | 10 | Rails.application.configure do 11 | # Settings specified here will take precedence over those in config/application.rb. 12 | 13 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 14 | config.cache_classes = true 15 | 16 | # Eager loading loads your whole application. When running a single test locally, 17 | # this probably isn't necessary. It's a good idea to do in a continuous integration 18 | # system, or in some way before deploying your code. 19 | config.eager_load = ENV['CI'].present? 20 | 21 | # Configure public file server for tests with Cache-Control for performance. 22 | config.public_file_server.enabled = true 23 | config.public_file_server.headers = { 24 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 25 | } 26 | 27 | # Show full error reports and disable caching. 28 | config.consider_all_requests_local = true 29 | config.action_controller.perform_caching = false 30 | config.cache_store = :null_store 31 | 32 | # Raise exceptions instead of rendering exception templates. 33 | config.action_dispatch.show_exceptions = false 34 | 35 | # Disable request forgery protection in test environment. 36 | config.action_controller.allow_forgery_protection = false 37 | 38 | # Store uploaded files on the local file system in a temporary directory. 39 | config.active_storage.service = :test 40 | 41 | config.action_mailer.perform_caching = false 42 | 43 | # Tell Action Mailer not to deliver emails to the real world. 44 | # The :test delivery method accumulates sent emails in the 45 | # ActionMailer::Base.deliveries array. 46 | config.action_mailer.delivery_method = :test 47 | 48 | # Print deprecation notices to the stderr. 49 | config.active_support.deprecation = :stderr 50 | 51 | # Raise exceptions for disallowed deprecations. 52 | config.active_support.disallowed_deprecation = :raise 53 | 54 | # Tell Active Support which deprecation messages to disallow. 55 | config.active_support.disallowed_deprecation_warnings = [] 56 | 57 | # Raises error for missing translations. 58 | # config.i18n.raise_on_missing_translations = true 59 | 60 | # Annotate rendered view with file names. 61 | # config.action_view.annotate_rendered_view_with_filenames = true 62 | end 63 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Avoid CORS issues when API is called from the frontend app. 6 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 7 | 8 | # Read more: https://github.com/cyu/rack-cors 9 | 10 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 11 | allow do 12 | origins '*' 13 | 14 | resource '*', 15 | headers: :any, 16 | methods: [:get, :post, :put, :patch, :delete, :options, :head] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure parameters to be filtered from the log file. Use this to limit dissemination of 6 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 7 | # notations and behaviors. 8 | Rails.application.config.filter_parameters += [ 9 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 10 | ] 11 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Be sure to restart your server when you modify this file. 3 | 4 | # Add new inflection rules using the following format. Inflections 5 | # are locale specific, and you may define rules for as many different 6 | # locales as you wish. All of these examples are active by default: 7 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 8 | # inflect.plural /^(ox)$/i, "\\1en" 9 | # inflect.singular /^(ox)en/i, "\\1" 10 | # inflect.irregular "person", "people" 11 | # inflect.uncountable %w( fish sheep ) 12 | # end 13 | 14 | # These inflection rules are supported but not enabled by default: 15 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 16 | # inflect.acronym "RESTful" 17 | # end 18 | -------------------------------------------------------------------------------- /config/initializers/kaminari_config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Kaminari.configure do |config| 4 | # config.default_per_page = 25 5 | # config.max_per_page = nil 6 | # config.window = 4 7 | # config.outer_window = 0 8 | # config.left = 0 9 | # config.right = 0 10 | # config.page_method_name = :page 11 | # config.param_name = :page 12 | # config.max_pages = nil 13 | # config.params_on_first_page = false 14 | end 15 | -------------------------------------------------------------------------------- /config/initializers/ransack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Ransack.configure do |config| 4 | # Change default search parameter key name. 5 | # Default key name is :q 6 | config.search_key = :query 7 | end 8 | -------------------------------------------------------------------------------- /config/locales/contracts/tr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tr: 3 | contracts: 4 | params: 5 | email: E-posta 6 | password: Şifre 7 | password_confirmation: Şifre onayı 8 | reset_password_token: Şifre sıfırlama jetonu 9 | -------------------------------------------------------------------------------- /config/locales/defaults/tr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tr: 3 | datetime: 4 | distance_in_words: 5 | about_x_hours: 6 | one: yaklaşık 1 saat 7 | other: yaklaşık %{count} saat 8 | about_x_months: 9 | one: yaklaşık 1 ay 10 | other: yaklaşık %{count} ay 11 | about_x_years: 12 | one: yaklaşık 1 yıl 13 | other: yaklaşık %{count} yıl 14 | almost_x_years: 15 | one: neredeyse 1 yıl 16 | other: neredeyse %{count} yıl 17 | half_a_minute: yarım dakika 18 | less_than_x_minutes: 19 | one: 1 dakikadan az 20 | other: "%{count} dakikadan az" 21 | less_than_x_seconds: 22 | one: 1 saniyeden az 23 | other: "%{count} saniyeden az" 24 | over_x_years: 25 | one: 1 yıldan fazla 26 | other: "%{count} yıldan fazla" 27 | x_days: 28 | one: 1 gün 29 | other: "%{count} gün" 30 | x_minutes: 31 | one: 1 dakika 32 | other: "%{count} dakika" 33 | x_months: 34 | one: 1 ay 35 | other: "%{count} ay" 36 | x_seconds: 37 | one: 1 saniye 38 | other: "%{count} saniye" 39 | x_years: 40 | one: 1 yıl 41 | other: "%{count} yıl" 42 | prompts: 43 | day: Gün 44 | hour: Saat 45 | minute: Dakika 46 | month: Ay 47 | second: Saniye 48 | year: Yıl 49 | time_helper: 50 | ago: 'önce' 51 | time: 52 | am: 'öğleden önce' 53 | formats: 54 | default: "%a, %d %b %Y %H:%M:%S %z" 55 | short: "%d %b %H:%M" 56 | long: "%d %B, %Y %H:%M" 57 | pm: 'öğleden sonra' 58 | date: 59 | abbr_day_names: 60 | - Paz 61 | - Pzt 62 | - Sal 63 | - Çar 64 | - Per 65 | - Cum 66 | - Cts 67 | abbr_month_names: 68 | - 69 | - Ocak 70 | - Şubat 71 | - Mart 72 | - Nisan 73 | - Mayıs 74 | - Haziran 75 | - Temmuz 76 | - Ağustos 77 | - Eylül 78 | - Ekim 79 | - Kasım 80 | - Aralık 81 | day_names: 82 | - Pazar 83 | - Pazartesi 84 | - Salı 85 | - Çarşamba 86 | - Perşembe 87 | - Cuma 88 | - Cumartesi 89 | formats: 90 | default: "%m-%d-%Y" 91 | long: "%B %d, %Y" 92 | short: "%b %d" 93 | month_names: 94 | - 95 | - Ocak 96 | - Şubat 97 | - Mart 98 | - Nisan 99 | - Mayıs 100 | - Haziran 101 | - Temmuz 102 | - Ağustos 103 | - Eylül 104 | - Ekim 105 | - Kasım 106 | - Aralık 107 | order: 108 | - :month 109 | - :day 110 | - :year 111 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /config/locales/gems/devise/en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/heartcombo/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 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | email_changed: 27 | subject: "Email Changed" 28 | password_change: 29 | subject: "Password Changed" 30 | omniauth_callbacks: 31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 32 | success: "Successfully authenticated from %{kind} account." 33 | passwords: 34 | 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." 35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 36 | 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." 37 | updated: "Your password has been changed successfully. You are now signed in." 38 | updated_not_active: "Your password has been changed successfully." 39 | registrations: 40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 41 | signed_up: "Welcome! You have signed up successfully." 42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 44 | 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." 45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address." 46 | updated: "Your account has been updated successfully." 47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again." 48 | sessions: 49 | signed_in: "Signed in successfully." 50 | signed_out: "Signed out successfully." 51 | already_signed_out: "Signed out successfully." 52 | unlocks: 53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 56 | errors: 57 | messages: 58 | already_confirmed: "was already confirmed, please try signing in" 59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 60 | expired: "has expired, please request a new one" 61 | not_found: "not found" 62 | not_locked: "was not locked" 63 | not_saved: 64 | one: "1 error prohibited this %{resource} from being saved:" 65 | other: "%{count} errors prohibited this %{resource} from being saved:" 66 | -------------------------------------------------------------------------------- /config/locales/gems/doorkeeper/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activerecord: 3 | attributes: 4 | doorkeeper/application: 5 | name: 'Name' 6 | redirect_uri: 'Redirect URI' 7 | errors: 8 | models: 9 | doorkeeper/application: 10 | attributes: 11 | redirect_uri: 12 | fragment_present: 'cannot contain a fragment.' 13 | invalid_uri: 'must be a valid URI.' 14 | unspecified_scheme: 'must specify a scheme.' 15 | relative_uri: 'must be an absolute URI.' 16 | secured_uri: 'must be an HTTPS/SSL URI.' 17 | forbidden_uri: 'is forbidden by the server.' 18 | scopes: 19 | not_match_configured: "doesn't match configured on the server." 20 | 21 | doorkeeper: 22 | applications: 23 | confirmations: 24 | destroy: 'Are you sure?' 25 | buttons: 26 | edit: 'Edit' 27 | destroy: 'Destroy' 28 | submit: 'Submit' 29 | cancel: 'Cancel' 30 | authorize: 'Authorize' 31 | form: 32 | error: 'Whoops! Check your form for possible errors' 33 | help: 34 | confidential: 'Application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential.' 35 | redirect_uri: 'Use one line per URI' 36 | blank_redirect_uri: "Leave it blank if you configured your provider to use Client Credentials, Resource Owner Password Credentials or any other grant type that doesn't require redirect URI." 37 | scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' 38 | edit: 39 | title: 'Edit application' 40 | index: 41 | title: 'Your applications' 42 | new: 'New Application' 43 | name: 'Name' 44 | callback_url: 'Callback URL' 45 | confidential: 'Confidential?' 46 | actions: 'Actions' 47 | confidentiality: 48 | 'yes': 'Yes' 49 | 'no': 'No' 50 | new: 51 | title: 'New Application' 52 | show: 53 | title: 'Application: %{name}' 54 | application_id: 'UID' 55 | secret: 'Secret' 56 | secret_hashed: 'Secret hashed' 57 | scopes: 'Scopes' 58 | confidential: 'Confidential' 59 | callback_urls: 'Callback urls' 60 | actions: 'Actions' 61 | not_defined: 'Not defined' 62 | 63 | authorizations: 64 | buttons: 65 | authorize: 'Authorize' 66 | deny: 'Deny' 67 | error: 68 | title: 'An error has occurred' 69 | new: 70 | title: 'Authorization required' 71 | prompt: 'Authorize %{client_name} to use your account?' 72 | able_to: 'This application will be able to' 73 | show: 74 | title: 'Authorization code' 75 | form_post: 76 | title: 'Submit this form' 77 | 78 | authorized_applications: 79 | confirmations: 80 | revoke: 'Are you sure?' 81 | buttons: 82 | revoke: 'Revoke' 83 | index: 84 | title: 'Your authorized applications' 85 | application: 'Application' 86 | created_at: 'Created At' 87 | date_format: '%Y-%m-%d %H:%M:%S' 88 | 89 | pre_authorization: 90 | status: 'Pre-authorization' 91 | 92 | errors: 93 | messages: 94 | # Common error messages 95 | invalid_request: 96 | unknown: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' 97 | missing_param: 'Missing required parameter: %{value}.' 98 | request_not_authorized: 'Request need to be authorized. Required parameter for authorizing request is missing or invalid.' 99 | invalid_redirect_uri: "The requested redirect uri is malformed or doesn't match client redirect URI." 100 | unauthorized_client: 'The client is not authorized to perform this request using this method.' 101 | access_denied: 'The resource owner or authorization server denied the request.' 102 | invalid_scope: 'The requested scope is invalid, unknown, or malformed.' 103 | invalid_code_challenge_method: 'The code challenge method must be plain or S256.' 104 | server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' 105 | temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' 106 | 107 | # Configuration error messages 108 | credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' 109 | resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfigured.' 110 | admin_authenticator_not_configured: 'Access to admin panel is forbidden due to Doorkeeper.configure.admin_authenticator being unconfigured.' 111 | 112 | # Access grant errors 113 | unsupported_response_type: 'The authorization server does not support this response type.' 114 | unsupported_response_mode: 'The authorization server does not support this response mode.' 115 | 116 | # Access token errors 117 | invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' 118 | invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' 119 | unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' 120 | 121 | invalid_token: 122 | revoked: "The access token was revoked" 123 | expired: "The access token expired" 124 | unknown: "The access token is invalid" 125 | revoke: 126 | unauthorized: "You are not authorized to revoke this token" 127 | 128 | forbidden_token: 129 | missing_scope: 'Access to this resource requires scope "%{oauth_scopes}".' 130 | 131 | flash: 132 | applications: 133 | create: 134 | notice: 'Application created.' 135 | destroy: 136 | notice: 'Application deleted.' 137 | update: 138 | notice: 'Application updated.' 139 | authorized_applications: 140 | destroy: 141 | notice: 'Application revoked.' 142 | 143 | layouts: 144 | admin: 145 | title: 'Doorkeeper' 146 | nav: 147 | oauth2_provider: 'OAuth2 Provider' 148 | applications: 'Applications' 149 | home: 'Home' 150 | application: 151 | title: 'OAuth authorization required' 152 | -------------------------------------------------------------------------------- /config/locales/gems/doorkeeper/tr.yml: -------------------------------------------------------------------------------- 1 | tr: 2 | activerecord: 3 | attributes: 4 | doorkeeper/application: 5 | name: 'Ad' 6 | redirect_uri: 'Yönlendirme URL' 7 | errors: 8 | models: 9 | doorkeeper/application: 10 | attributes: 11 | redirect_uri: 12 | fragment_present: 'Yönlendirme URL için fragment kullanılamaz' 13 | invalid_uri: 'Geçerli bir URI olmalıdır' 14 | unspecified_scheme: 'Şema belirtilmelidir' 15 | relative_uri: 'Mutlak URI olmalıdır' 16 | secured_uri: 'HTTPS/SSL URI olmalıdır' 17 | forbidden_uri: 'Sunucu tarafından yasaklanmıştır' 18 | scopes: 19 | not_match_configured: "Sunucu tarafında yapılandırılmış olan ile eşleşmiyor" 20 | doorkeeper: 21 | applications: 22 | confirmations: 23 | destroy: 'Silmek istediğinizden emin misiniz?' 24 | buttons: 25 | edit: 'Düzenle' 26 | destroy: 'Sil' 27 | submit: 'Gönder' 28 | cancel: 'İptal' 29 | authorize: 'Yetkilendir' 30 | form: 31 | error: 'Hata! Formunuzda hata olabilir' 32 | help: 33 | confidential: 'Gizli olarak kullanılacak bir uygulama olarak işaretlenir. Mobil uygulamalar veya tek sayfalık uygulamalar gibi.' 34 | redirect_uri: 'Her URI için bir satır kullanın' 35 | blank_redirect_uri: "Eğer uygulama için Client Credentials, Resource Owner Password Credentials veya başka bir yetki türü kullanılıyorsa, yönlendirme URI'lerini boş bırakın." 36 | scopes: 'Yetkileri aralarında boşluk bırakarak ayırın' 37 | edit: 38 | title: 'Uygulamayı düzenle' 39 | index: 40 | title: 'Uygulamalarınız' 41 | new: 'Yeni Uygulama' 42 | name: 'Ad' 43 | callback_url: 'Yönlendirme URL' 44 | confidential: 'Gizli?' 45 | actions: 'Eylemler' 46 | confidentiality: 47 | 'yes': 'Evet' 48 | 'no': 'Hayır' 49 | new: 50 | title: 'Yeni Uygulama' 51 | show: 52 | title: "Uygulama: %{name}" 53 | application_id: 'UID' 54 | secret: 'Gizli' 55 | secret_hashed: 'Gizli hashed' 56 | scopes: 'Yetkiler' 57 | confidential: 'Gizli' 58 | callback_urls: 'Yönlendirme URLleri' 59 | actions: 'Eylemler' 60 | not_defined: 'Belirtilmemiş' 61 | 62 | authorizations: 63 | buttons: 64 | authorize: 'Yetkilendir' 65 | deny: 'Reddet' 66 | error: 67 | title: 'Bir hata oluştu' 68 | new: 69 | title: 'Oturum zorunlu ' 70 | prompt: "%{client_name} kullanıcı hesabınıza yetkilendirmek istiyor musunuz?" 71 | able_to: 'Bu uygulama şunu yapabilir' 72 | show: 73 | title: 'Yetkilendirme kodu' 74 | form_post: 75 | title: 'Bu formu gönderin' 76 | 77 | authorized_applications: 78 | confirmations: 79 | revoke: 'Emin misiniz?' 80 | buttons: 81 | revoke: 'İptal et' 82 | index: 83 | title: 'Yetkilendirilmiş uygulamalar' 84 | application: 'Uygulama' 85 | created_at: 'Oluşturulma tarihi' 86 | date_format: '%Y-%m-%d %H:%M:%S' 87 | 88 | pre_authorization: 89 | status: 'Yetkilendirme durumu' 90 | 91 | errors: 92 | messages: 93 | # Common error messages 94 | invalid_request: 95 | unknown: 'İstekte gerekli bir parametre eksik, desteklenmeyen bir parametre değeri içeriyor veya başka bir şekilde hatalı biçimlendirilmiş.' 96 | missing_param: "%{value} parametresi eksik" 97 | request_not_authorized: 'Talebin yetkilendirilmesi gerekiyor. Yetkilendirme isteği için gerekli parametre eksik veya geçersiz.' 98 | invalid_redirect_uri: "İstenen yönlendirme URI'si hatalı biçimlendirilmiş veya istemci yönlendirme URI'si ile eşleşmiyor." 99 | unauthorized_client: 'İstemci, bu yöntemi kullanarak bu isteği gerçekleştirme yetkisine sahip değil.' 100 | access_denied: 'Kaynak sahibi veya yetkilendirme sunucusu, isteği reddetti.' 101 | invalid_scope: 'İstenen kapsam geçersiz, bilinmiyor veya hatalı biçimlendirilmiş.' 102 | invalid_code_challenge_method: 'Kod sorgulama yöntemi düz veya S256 olmalıdır.' 103 | server_error: 'Yetkilendirme sunucusu, isteği yerine getirmesini engelleyen beklenmeyen bir koşulla karşılaştı.' 104 | temporarily_unavailable: 'Yetkilendirme sunucusu, sunucunun geçici olarak aşırı yüklenmesi veya bakımı nedeniyle şu anda isteği işleyemiyor.' 105 | 106 | # Configuration error messages 107 | credential_flow_not_configured: 'Doorkeeper.configure.resource_owner_from_credentials yapılandırılmadığından Kaynak Sahibi Parolası Kimlik Bilgileri akışı başarısız oldu.' 108 | resource_owner_authenticator_not_configured: 'Doorkeeper.configure.resource_owner_authenticator yapılandırılmadığından Kaynak Sahibi bulma başarısız oldu.' 109 | admin_authenticator_not_configured: 'Doorkeeper.configure.admin_authenticator yapılandırılmamış olduğundan yönetici paneline erişim yasaktır.' 110 | 111 | # Access grant errors 112 | unsupported_response_type: 'Yetkilendirme sunucusu bu yanıt türünü desteklemiyor.' 113 | unsupported_response_mode: 'Yetkilendirme sunucusu bu yanıt modunu desteklemiyor.' 114 | 115 | # Access token errors 116 | invalid_client: 'Bilinmeyen istemci, istemci kimlik doğrulamasının dahil edilmemesi veya desteklenmeyen kimlik doğrulama yöntemi nedeniyle istemci kimlik doğrulaması başarısız oldu.' 117 | invalid_grant: "Sağlanan yetkilendirme yetkisi geçersiz, süresi doldu, iptal edildi, yetkilendirme talebinde kullanılan yeniden yönlendirme URI'si ile eşleşmiyor veya başka bir istemciye verildi." 118 | unsupported_grant_type: 'Yetki verme türü, yetkilendirme sunucusu tarafından desteklenmiyor.' 119 | 120 | invalid_token: 121 | revoked: "Erişim tokeni geri alındı." 122 | expired: "Erişim tokeni zaman aşımına uğradı ve kullanılmaya devam edemez." 123 | unknown: "Erişim tokeni geçersiz." 124 | revoke: 125 | unauthorized: "Bu erişim tokeni için yetkilendirme yapılamadı." 126 | 127 | forbidden_token: 128 | missing_scope: "Bu kaynağa erişim için, %{oauth_scopes} yetkileri gerekli." 129 | 130 | flash: 131 | applications: 132 | create: 133 | notice: 'Uygulama oluşturuldu' 134 | destroy: 135 | notice: 'Uygulama silindi' 136 | update: 137 | notice: 'Uygulama güncellendi' 138 | authorized_applications: 139 | destroy: 140 | notice: 'Yetkilendirilmiş uygulama silindi' 141 | 142 | layouts: 143 | admin: 144 | title: 'Doorkeeper' 145 | nav: 146 | oauth2_provider: 'OAuth2 Sağlayıcı' 147 | applications: 'Uygulamalar' 148 | home: 'Anasayfa' 149 | application: 150 | title: 'OAuth yetkilendirmesi zorunlu' 151 | -------------------------------------------------------------------------------- /config/locales/gems/dry_validation/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | dry_validation: 4 | keys_splitter: ' of ' 5 | errors: 6 | same_password?: does not match 7 | -------------------------------------------------------------------------------- /config/locales/gems/dry_validation/tr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tr: 3 | dry_validation: 4 | keys_splitter: ' içindeki ' 5 | errors: 6 | same_password?: eşleşmiyor 7 | array?: dizi olmalı 8 | attr?: eksik 9 | bool?: true veya false olmalı 10 | date?: tarih olmalı 11 | date_time?: saat - tarih olmalı 12 | decimal?: ondalık sayı olmalı 13 | empty?: boş olmalı 14 | eql?: '%{left} ile eşit olmalı' 15 | even?: çift sayı olmalı 16 | excluded_from?: 17 | arg: 18 | default: '%{list} değerlerinden biri olmalı' 19 | range: '%{list_left} - %{list_right} arasında olmalı' 20 | excludes?: '%{value} değerini içermemeli' 21 | exclusion?: '%{list} değerlerinden biri olmamalı' 22 | false?: false olmalı 23 | filled?: doldurulmalı 24 | float?: ondalık sayı olmalı 25 | format?: geçersiz formata sahip 26 | gt?: "%{num}'dan büyük olmalı" 27 | gteq?: "%{num}'dan büyük ya da eşit olmalı" 28 | hash?: must be a hash 29 | included_in?: 30 | arg: 31 | default: "%{list} seçeneklerinden biri olmalı" 32 | range: "%{list_left} - %{list_right} seçeneklerinden biri olmalı" 33 | includes?: "%{value} içermeli" 34 | inclusion?: "%{list} seçeneklerinden biri olmalı" 35 | int?: sayı olmalı 36 | invalid_permission: geçersiz yetki 37 | key?: eksik 38 | lt?: "%{num} sayısından küçük olmalı" 39 | lteq?: "%{num} sayısından küçük ya da eşit olmalı" 40 | max_size?: "%{num} karakterden büyük olamaz" 41 | min_size?: "%{num} karakterden küçük olamaz" 42 | must_have_same_currency: aynı döviz cinsi seçilmeli ya da Türk Lirası olarak hesapla seçeneği kullanılmalı 43 | must_have_valid_email: geçersiz 44 | must_have_valid_url: geçersiz 45 | none?: boş olamaz 46 | not_eql?: '%{left} ile eşit olmamalı' 47 | number?: sayı olmalı 48 | odd?: tek sayı olmalı 49 | or: veya 50 | password_not_matched: uyuşmuyor 51 | size?: 52 | arg: 53 | default: "%{size} hane olmalı" 54 | range: "%{size_left} - %{size_right} karakter aralığında olmalı" 55 | value: 56 | string: 57 | arg: 58 | default: uzunluğu %{size} karakter olmalı 59 | range: uzunluğu %{size_left} - %{size_right} karakter aralığında olmalı 60 | str?: alfabetik karakter olmalı 61 | time?: saat olmalı 62 | true?: true olmalı 63 | type?: "%{type} tipinde olmalı" 64 | unit_required: boş olmamalı 65 | -------------------------------------------------------------------------------- /config/locales/services/en.yml: -------------------------------------------------------------------------------- 1 | --- 2 | en: 3 | application_service: 4 | default: 5 | find_resource: 6 | error: '%{klass} could not be found' 7 | -------------------------------------------------------------------------------- /config/locales/services/tr.yml: -------------------------------------------------------------------------------- 1 | --- 2 | tr: 3 | application_service: 4 | default: 5 | find_resource: 6 | error: '%{klass} bulunamadı' 7 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Puma can serve each request in a thread from an internal thread pool. 4 | # The `threads` method setting takes two numbers: a minimum and maximum. 5 | # Any libraries that use thread pools should be configured to match 6 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 7 | # and maximum; this matches the default thread size of Active Record. 8 | # 9 | max_threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 } 10 | min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count } 11 | threads min_threads_count, max_threads_count 12 | 13 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 14 | # terminating a worker in development environments. 15 | # 16 | worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development' 17 | 18 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 19 | # 20 | port ENV.fetch('PORT') { 3000 } 21 | 22 | # Specifies the `environment` that Puma will run in. 23 | # 24 | environment ENV.fetch('RAILS_ENV') { 'development' } 25 | 26 | # Specifies the `pidfile` that Puma will use. 27 | pidfile ENV.fetch('PIDFILE') { 'tmp/pids/server.pid' } 28 | 29 | # Specifies the number of `workers` to boot in clustered mode. 30 | # Workers are forked web server processes. If using threads and workers together 31 | # the concurrency of the application would be max `threads` * `workers`. 32 | # Workers do not work on JRuby or Windows (both of which do not support 33 | # processes). 34 | # 35 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 36 | 37 | # Use the `preload_app!` method when specifying a `workers` number. 38 | # This directive tells Puma to first boot the application and load code 39 | # before forking the application. This takes advantage of Copy On Write 40 | # process behavior so workers use less memory. 41 | # 42 | # preload_app! 43 | 44 | # Allow puma to be restarted by `bin/rails restart` command. 45 | plugin :tmp_restart 46 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 5 | 6 | # Defines the root path route ("/") 7 | root 'swagger#index' 8 | 9 | # Swagger documentation 10 | draw :swagger 11 | 12 | # API routes 13 | defaults format: :json do 14 | # V1 15 | namespace :v1 do 16 | draw :authentication 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /config/routes/authentication.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | devise_for :users, skip: :all 4 | 5 | namespace :users do 6 | use_doorkeeper do 7 | controllers tokens: 'tokens' 8 | skip_controllers :applications, :authorized_applications, :authorizations 9 | end 10 | 11 | post 'sign_up', to: 'registrations#create', as: :registration 12 | post 'password', to: 'passwords#create', as: :password 13 | patch 'password', to: 'passwords#update', as: nil 14 | end 15 | -------------------------------------------------------------------------------- /config/routes/swagger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | scope :swagger, as: 'swagger' do 4 | get '/', to: 'swagger#index', as: :root 5 | get '/v1_data', to: 'swagger#v1_data', as: :v1_data 6 | get '/v2_data', to: 'swagger#v2_data', as: :v2_data 7 | end 8 | -------------------------------------------------------------------------------- /config/sidekiq.yml: -------------------------------------------------------------------------------- 1 | # Sample configuration file for Sidekiq. 2 | # Options here can still be overridden by cmd line args. 3 | # Place this file at config/sidekiq.yml and Sidekiq will 4 | # pick it up automatically. 5 | --- 6 | :verbose: false 7 | :concurrency: <%= ENV['SIDEKIQ_CONCURRENCY'] || 10 %> 8 | 9 | # Set timeout to 8 on Heroku, longer if you manage your own systems. 10 | :timeout: 8 11 | 12 | # Sidekiq will run this file through ERB when reading it so you can 13 | # even put in dynamic logic, like a host-specific queue. 14 | # http://www.mikeperham.com/2013/11/13/advanced-sidekiq-host-specific-queues/ 15 | :queues: 16 | - [critical, 5] 17 | - [default, 2] 18 | - [low, 1] 19 | # you can override concurrency based on environment 20 | # production: 21 | # :concurrency: 25 22 | # staging: 23 | # :concurrency: 15 24 | -------------------------------------------------------------------------------- /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 bin/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-<%= Rails.env %> 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-<%= Rails.env %> 23 | 24 | # Use bin/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-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /db/migrate/20220212185845_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DeviseCreateUsers < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :users, id: :uuid do |t| 6 | ## Database authenticatable 7 | t.string :email, null: false, default: '' 8 | t.string :encrypted_password, null: false, default: '' 9 | 10 | ## Recoverable 11 | t.string :reset_password_token 12 | t.datetime :reset_password_sent_at 13 | 14 | ## Rememberable 15 | t.datetime :remember_created_at 16 | 17 | ## Trackable 18 | t.integer :sign_in_count, default: 0, null: false 19 | t.datetime :current_sign_in_at 20 | t.datetime :last_sign_in_at 21 | t.string :current_sign_in_ip 22 | t.string :last_sign_in_ip 23 | 24 | ## Confirmable 25 | # t.string :confirmation_token 26 | # t.datetime :confirmed_at 27 | # t.datetime :confirmation_sent_at 28 | # t.string :unconfirmed_email # Only if using reconfirmable 29 | 30 | ## Lockable 31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 32 | # t.string :unlock_token # Only if unlock strategy is :email or :both 33 | # t.datetime :locked_at 34 | 35 | t.timestamps null: false 36 | end 37 | 38 | add_index :users, :email, unique: true 39 | add_index :users, :reset_password_token, unique: true 40 | # add_index :users, :confirmation_token, unique: true 41 | # add_index :users, :unlock_token, unique: true 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /db/migrate/20220212195921_create_doorkeeper_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateDoorkeeperTables < ActiveRecord::Migration[7.0] 4 | def change 5 | create_table :oauth_applications, id: :uuid do |t| 6 | t.string :name, null: false 7 | t.string :uid, null: false 8 | t.string :secret, null: false 9 | 10 | # Remove `null: false` if you are planning to use grant flows 11 | # that doesn't require redirect URI to be used during authorization 12 | # like Client Credentials flow or Resource Owner Password. 13 | t.text :redirect_uri 14 | t.string :scopes, null: false, default: '' 15 | t.boolean :confidential, null: false, default: true 16 | t.timestamps null: false 17 | end 18 | 19 | add_index :oauth_applications, :uid, unique: true 20 | 21 | create_table :oauth_access_tokens, id: :uuid do |t| 22 | t.references :resource_owner, index: true, polymorphic: true, type: :uuid 23 | 24 | # Remove `null: false` if you are planning to use Password 25 | # Credentials Grant flow that doesn't require an application. 26 | t.references :application, null: false, type: :uuid 27 | 28 | # If you use a custom token generator you may need to change this column 29 | # from string to text, so that it accepts tokens larger than 255 30 | # characters. More info on custom token generators in: 31 | # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator 32 | # 33 | # t.text :token, null: false 34 | t.string :token, null: false 35 | 36 | t.string :refresh_token 37 | t.integer :expires_in 38 | t.datetime :revoked_at 39 | t.datetime :created_at, null: false 40 | t.string :scopes 41 | 42 | # The authorization server MAY issue a new refresh token, in which case 43 | # *the client MUST discard the old refresh token* and replace it with the 44 | # new refresh token. The authorization server MAY revoke the old 45 | # refresh token after issuing a new refresh token to the client. 46 | # @see https://datatracker.ietf.org/doc/html/rfc6749#section-6 47 | # 48 | # Doorkeeper implementation: if there is a `previous_refresh_token` column, 49 | # refresh tokens will be revoked after a related access token is used. 50 | # If there is no `previous_refresh_token` column, previous tokens are 51 | # revoked as soon as a new access token is created. 52 | # 53 | # Comment out this line if you want refresh tokens to be instantly 54 | # revoked after use. 55 | t.string :previous_refresh_token, null: false, default: '' 56 | end 57 | 58 | add_index :oauth_access_tokens, :token, unique: true 59 | add_index :oauth_access_tokens, :refresh_token, unique: true 60 | 61 | add_foreign_key( 62 | :oauth_access_tokens, 63 | :oauth_applications, 64 | column: :application_id 65 | ) 66 | 67 | # Uncomment below to ensure a valid reference to the resource owner's table 68 | # add_foreign_key :oauth_access_grants, , column: :resource_owner_id 69 | # add_foreign_key :oauth_access_tokens, , column: :resource_owner_id 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /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 `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/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[7.0].define(version: 2022_02_12_195921) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "oauth_access_tokens", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| 18 | t.string "resource_owner_type" 19 | t.uuid "resource_owner_id" 20 | t.uuid "application_id", null: false 21 | t.string "token", null: false 22 | t.string "refresh_token" 23 | t.integer "expires_in" 24 | t.datetime "revoked_at" 25 | t.datetime "created_at", null: false 26 | t.string "scopes" 27 | t.string "previous_refresh_token", default: "", null: false 28 | t.index ["application_id"], name: "index_oauth_access_tokens_on_application_id" 29 | t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true 30 | t.index ["resource_owner_type", "resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner" 31 | t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true 32 | end 33 | 34 | create_table "oauth_applications", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| 35 | t.string "name", null: false 36 | t.string "uid", null: false 37 | t.string "secret", null: false 38 | t.text "redirect_uri" 39 | t.string "scopes", default: "", null: false 40 | t.boolean "confidential", default: true, null: false 41 | t.datetime "created_at", null: false 42 | t.datetime "updated_at", null: false 43 | t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true 44 | end 45 | 46 | create_table "users", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| 47 | t.string "email", default: "", null: false 48 | t.string "encrypted_password", default: "", null: false 49 | t.string "reset_password_token" 50 | t.datetime "reset_password_sent_at" 51 | t.datetime "remember_created_at" 52 | t.integer "sign_in_count", default: 0, null: false 53 | t.datetime "current_sign_in_at" 54 | t.datetime "last_sign_in_at" 55 | t.string "current_sign_in_ip" 56 | t.string "last_sign_in_ip" 57 | t.datetime "created_at", null: false 58 | t.datetime "updated_at", null: false 59 | t.index ["email"], name: "index_users_on_email", unique: true 60 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true 61 | end 62 | 63 | add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id" 64 | end 65 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # This file should contain all the record creation needed to seed the database with its default values. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Examples: 6 | # 7 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 8 | # Character.create(name: "Luke", movie: movies.first) 9 | -------------------------------------------------------------------------------- /docs/CONTRACT.md: -------------------------------------------------------------------------------- 1 | # Contract Generator Documentation 2 | This boilerplate is using `dry-validation` gem for contract structure. If you need contract, you can use custom contract generator like that: 3 | ```bash 4 | $ rails generate contract Users::Registration 5 | ``` 6 | 7 | Then, it will create some service classes for your operations. 8 | ``` 9 | create app/contracts/users/registration_contract.rb 10 | create test/contracts/users/registration_contract_test.rb 11 | ``` 12 | 13 | You can access service templates from `/lib/generators/contract` folder. 14 | -------------------------------------------------------------------------------- /docs/RANSACK.md: -------------------------------------------------------------------------------- 1 | # Search & Filter & Sort (RANSACK) 2 | 3 | The search parameters are passed to ransack as a hash. The URL representation of this hash uses the bracket notation: ```hash_name[key]=value```. The hash_name is the parameter which is defined in the controller, for instance ```query```. The key is the attribute and search predicate compound, for instance ```first_name_cont```, the value is the search parameter. When searching without using the search form helpers this URL structure needs to be created manually. 4 | 5 | For example, the URL layout for searching and sorting users could looks like this: 6 | 7 | ``` 8 | /users?query[first_name_cont]=pete&query[last_name_cont]=jack&query[s]=created_at+desc 9 | ``` 10 | 11 | _Note that the sorting parameter ```s``` is nested within the ```query``` hash._ 12 | 13 | When using JavaScript to create such a URL, a matching axios request could look like this: 14 | 15 | ```javascript 16 | import axios from 'axios'; 17 | 18 | const response = await axios.get('/users', { 19 | params: { 20 | query: { 21 | first_name_cont: "pete", 22 | last_name_cont: "jack", 23 | s: "created_at desc" 24 | } 25 | } 26 | }) 27 | ``` 28 | --- 29 | 30 | ## List of all possible predicates 31 | 32 | | Predicate | Description | Notes | 33 | | ------------- | ------------- |-------- | 34 | | `*_eq` | equal | | 35 | | `*_not_eq` | not equal | | 36 | | `*_matches` | matches with `LIKE` | e.g. `q[email_matches]=%@gmail.com`| 37 | | `*_does_not_match` | does not match with `LIKE` | | 38 | | `*_matches_any` | Matches any | | 39 | | `*_matches_all` | Matches all | | 40 | | `*_does_not_match_any` | Does not match any | | 41 | | `*_does_not_match_all` | Does not match all | | 42 | | `*_lt` | less than | | 43 | | `*_lteq` | less than or equal | | 44 | | `*_gt` | greater than | | 45 | | `*_gteq` | greater than or equal | | 46 | | `*_present` | not null and not empty | Only compatible with string columns. Example: `q[name_present]=1` (SQL: `col is not null AND col != ''`) | 47 | | `*_blank` | is null or empty. | (SQL: `col is null OR col = ''`) | 48 | | `*_null` | is null | | 49 | | `*_not_null` | is not null | | 50 | | `*_in` | match any values in array | e.g. `q[name_in][]=Alice&q[name_in][]=Bob` | 51 | | `*_not_in` | match none of values in array | | 52 | | `*_lt_any` | Less than any | SQL: `col < value1 OR col < value2` | 53 | | `*_lteq_any` | Less than or equal to any | | 54 | | `*_gt_any` | Greater than any | | 55 | | `*_gteq_any` | Greater than or equal to any | | 56 | | `*_lt_all` | Less than all | SQL: `col < value1 AND col < value2` | 57 | | `*_lteq_all` | Less than or equal to all | | 58 | | `*_gt_all` | Greater than all | | 59 | | `*_gteq_all` | Greater than or equal to all | | 60 | | `*_not_eq_all` | none of values in a set | | 61 | | `*_start` | Starts with | SQL: `col LIKE 'value%'` | 62 | | `*_not_start` | Does not start with | | 63 | | `*_start_any` | Starts with any of | | 64 | | `*_start_all` | Starts with all of | | 65 | | `*_not_start_any` | Does not start with any of | | 66 | | `*_not_start_all` | Does not start with all of | | 67 | | `*_end` | Ends with | SQL: `col LIKE '%value'` | 68 | | `*_not_end` | Does not end with | | 69 | | `*_end_any` | Ends with any of | | 70 | | `*_end_all` | Ends with all of | | 71 | | `*_not_end_any` | | | 72 | | `*_not_end_all` | | | 73 | | `*_cont` | Contains value | uses `LIKE` | 74 | | `*_cont_any` | Contains any of | | 75 | | `*_cont_all` | Contains all of | | 76 | | `*_not_cont` | Does not contain | 77 | | `*_not_cont_any` | Does not contain any of | | 78 | | `*_not_cont_all` | Does not contain all of | | 79 | | `*_i_cont` | Contains value with case insensitive | uses `ILIKE` | 80 | | `*_i_cont_any` | Contains any of values with case insensitive | | 81 | | `*_i_cont_all` | Contains all of values with case insensitive | | 82 | | `*_not_i_cont` | Does not contain with case insensitive | 83 | | `*_not_i_cont_any` | Does not contain any of values with case insensitive | | 84 | | `*_not_i_cont_all` | Does not contain all of values with case insensitive | | 85 | | `*_true` | is true | | 86 | | `*_false` | is false | | 87 | -------------------------------------------------------------------------------- /docs/SERVICE.md: -------------------------------------------------------------------------------- 1 | # Service Generator Documentation 2 | This boilerplate is using `dry-monads` gem for service structure. If you need service, you can use custom service generator like that: 3 | ```bash 4 | $ rails generate service Facebook::Adset 5 | ``` 6 | 7 | Then, it will create some service classes for your operations. 8 | ``` 9 | create app/services/facebook/adset_service.rb 10 | create test/services/facebook/adset_service_test.rb 11 | ``` 12 | 13 | You can access service templates from `/lib/generators/service` folder. 14 | 15 | # Sample list service usage 16 | ```ruby 17 | class UsersController < ApplicationController 18 | def index 19 | service = UserService::List.new(query: query_object, 20 | pagination: pagination_object).call 21 | 22 | @users = service.success 23 | end 24 | end 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/SWAGGER.md: -------------------------------------------------------------------------------- 1 | # Swagger Documentation 2 | This boilerplate is using `swagger-blocks` gem for creating swagger documentation with Ruby on Rails application. 3 | 4 | [TUTORIAL](https://duetcode.io/rails-api-only-course/api-documentation-with-swagger) 5 | 6 | ## Folder Structure 7 | . 8 | ├── ... 9 | ├── app 10 | │ ├── ... 11 | │ ├── controllers 12 | │ ├── swagger_docs 13 | │ │ ├── controllers 14 | │ │ ├── inputs 15 | │ │ ├── models 16 | │ │ ├── parameters 17 | │ │ ├── responses 18 | 19 | --- 20 | ### Sample controller 21 | ```ruby 22 | # frozen_string_literal: true 23 | 24 | module Controllers 25 | class CitiesController 26 | include Swagger::Blocks 27 | 28 | swagger_path '/cities/{city_id}' do 29 | operation :get do 30 | key :summary, 'Return city with by id' 31 | key :description, 'Return city with by id' 32 | key :operationId, 'showCity' 33 | key :tags, [ 34 | 'cities' 35 | ] 36 | 37 | parameter do 38 | key :'$ref', :city_id 39 | end 40 | 41 | response 200 do 42 | key :description, 'Successful response' 43 | content :'application/json' do 44 | schema do 45 | key :'$ref', :CityShowResponse 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | end 53 | ``` 54 | --- 55 | ### Sample input 56 | ```ruby 57 | # frozen_string_literal: true 58 | 59 | module Inputs 60 | module City 61 | class CreateInput 62 | include Swagger::Blocks 63 | 64 | swagger_component do 65 | schema :CityCreateInput do 66 | key :required, %i[city] 67 | 68 | property :city do 69 | key :required, %i[name alpha_2_code] 70 | 71 | property :name do 72 | key :type, :string 73 | key :example, 'Ankara' 74 | end 75 | 76 | property :alpha_2_code do 77 | key :type, :string 78 | key :example, 'TR-06' 79 | end 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | ``` 87 | --- 88 | ### Sample model 89 | ```ruby 90 | # frozen_string_literal: true 91 | 92 | module Models 93 | class City 94 | include Swagger::Blocks 95 | 96 | swagger_component do 97 | schema :City do 98 | key :type, :object 99 | key :required, %i[id name alpha_2_code] 100 | 101 | property :id do 102 | key :type, :string 103 | key :format, :uuid 104 | key :example, 'e7c8f8f0-e8e0-4b0f-b8b1-f8f8f8f8f8f8' 105 | end 106 | 107 | property :name do 108 | key :type, :string 109 | key :example, 'Ankara' 110 | end 111 | 112 | property :alpha_2_code do 113 | key :type, :string 114 | key :example, 'TR-06' 115 | end 116 | end 117 | end 118 | end 119 | end 120 | ``` 121 | --- 122 | ### Sample parameter 123 | ```ruby 124 | # frozen_string_literal: true 125 | 126 | module Parameters 127 | class CityId 128 | include Swagger::Blocks 129 | 130 | swagger_component do 131 | parameter :city_id do 132 | key :name, :city_id 133 | key :in, :path 134 | key :description, 'The id of the city' 135 | key :required, true 136 | schema do 137 | key :type, :string 138 | key :format, :uuid 139 | end 140 | end 141 | end 142 | end 143 | end 144 | ``` 145 | --- 146 | ### Sample response 147 | ```ruby 148 | # frozen_string_literal: true 149 | 150 | module Responses 151 | module City 152 | class Show 153 | include Swagger::Blocks 154 | 155 | swagger_component do 156 | schema :CityShowResponse do 157 | key :type, :object 158 | key :required, %i[city meta] 159 | 160 | property :city do 161 | key :'$ref', :City 162 | end 163 | end 164 | end 165 | end 166 | end 167 | end 168 | ``` 169 | -------------------------------------------------------------------------------- /docs/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/docs/cover.png -------------------------------------------------------------------------------- /lib/generators/contract/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a new contract 3 | 4 | Example: 5 | bin/rails generate contract Customer::Create email fullname 6 | 7 | This will create: 8 | app/contracts/customers/create_contract.rb 9 | app/test/contracts/customers/create_contract_test.rb 10 | -------------------------------------------------------------------------------- /lib/generators/contract/contract_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ContractGenerator < Rails::Generators::NamedBase 4 | argument :params, type: :array, default: [], banner: 'name:o fullname:required' 5 | class_option :parent, type: :string, default: 'ApplicationContract', desc: 'The parent class for the generated contract' 6 | 7 | check_class_collision suffix: 'Contract' 8 | 9 | source_root File.expand_path('templates', __dir__) 10 | 11 | def create_contract_files 12 | template 'contract.rb', File.join('app/contracts/', class_path, "#{file_name}_contract.rb") 13 | template 'contract_test.rb', File.join('test/contracts/', class_path, "#{file_name}_contract_test.rb") 14 | 15 | return unless Rails.env.development? 16 | 17 | system("rubocop -A #{contract_file_path}") 18 | system("rubocop -A #{contract_test_file_path}") 19 | end 20 | 21 | private 22 | 23 | def contract_file_path 24 | "app/contracts/#{class_path.join('/')}/#{file_name}_contract.rb" 25 | end 26 | 27 | def contract_test_file_path 28 | "test/contracts/#{class_path.join('/')}/#{file_name}_contract_test.rb" 29 | end 30 | 31 | def parent_class_name 32 | options[:parent] 33 | end 34 | 35 | def formatted_params 36 | params.map do |current_param| 37 | type = type_defined_for_param?(current_param) ? defined_type_for_param(current_param) : 'optional' 38 | 39 | [type, parse_param(current_param).first] 40 | end 41 | end 42 | 43 | def parse_param(current_param) 44 | current_param.split(':') 45 | end 46 | 47 | def type_defined_for_param?(current_param) 48 | param = parse_param(current_param) 49 | 50 | return false if param.size <= 1 51 | 52 | valid_types = %w[r o required optional] 53 | 54 | valid_types.include?(param.second) 55 | end 56 | 57 | def defined_type_for_param(current_param) 58 | param = parse_param(current_param) 59 | 60 | case param.last 61 | when 'o', 'optional' 62 | 'optional' 63 | when 'r', 'required' 64 | 'required' 65 | end 66 | end 67 | 68 | def file_name 69 | @_file_name ||= remove_possible_suffix(super) 70 | end 71 | 72 | def remove_possible_suffix(name) 73 | name.sub(/_?contract$/i, '') 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/generators/contract/templates/contract.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <% module_namespacing do -%> 4 | class <%= class_name %>Contract < <%= parent_class_name.classify %> 5 | params do 6 | <% formatted_params.each do |param| -%> 7 | <%= param.first %>(:<%= param.last %>) {} 8 | <% end -%> 9 | end 10 | end 11 | <% end -%> 12 | -------------------------------------------------------------------------------- /lib/generators/contract/templates/contract_test.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | <% module_namespacing do -%> 6 | class <%= class_name %>ContractTest < <%= ActiveSupport::TestCase %> 7 | CONTRACT = <%= class_name %>Contract.new 8 | 9 | def validate(payload = {}) 10 | CONTRACT.call(payload) 11 | end 12 | 13 | <% formatted_params.each do |param| -%> 14 | test 'validate#<%= param.last %>' do 15 | end 16 | 17 | <% end -%> 18 | end 19 | <% end -%> 20 | -------------------------------------------------------------------------------- /lib/generators/operation/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a new operation 3 | 4 | Example: 5 | bin/rails generate operation Customer::Create current_user params 6 | 7 | This will create: 8 | app/operations/customer/create_operation.rb 9 | test/operations/customer/create_operation_test.rb 10 | -------------------------------------------------------------------------------- /lib/generators/operation/operation_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class OperationGenerator < Rails::Generators::NamedBase 4 | argument :params, type: :array, default: [], banner: 'action action' 5 | class_option :parent, type: :string, default: 'ApplicationOperation', desc: 'The parent class for the generated operation' 6 | 7 | check_class_collision suffix: 'Operation' 8 | 9 | source_root File.expand_path('templates', __dir__) 10 | 11 | def create_operation_files 12 | template 'operation.rb', File.join('app/operations/', class_path, "#{file_name}_operation.rb") 13 | template 'operation_test.rb', File.join('test/operations/', class_path, "#{file_name}_operation_test.rb") 14 | 15 | return unless Rails.env.development? 16 | 17 | system("rubocop -A #{operation_file_path}") 18 | system("rubocop -A #{operation_test_file_path}") 19 | end 20 | 21 | private 22 | 23 | def operation_file_path 24 | "app/operations/#{class_path.join('/')}/#{file_name}_operation.rb" 25 | end 26 | 27 | def operation_test_file_path 28 | "test/operations/#{class_path.join('/')}/#{file_name}_operation_test.rb" 29 | end 30 | 31 | def parent_class_name 32 | options[:parent] 33 | end 34 | 35 | def file_name 36 | @_file_name ||= remove_possible_suffix(super) 37 | end 38 | 39 | def remove_possible_suffix(name) 40 | name.sub(/_?operation$/i, '') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/generators/operation/templates/operation.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <% module_namespacing do -%> 4 | class <%= class_name %>Service < <%= parent_class_name.classify %> 5 | <% params.each do |param| -%> 6 | option :<%= param %>, type: Types<%= param == 'params' ? '.Instance(ActionController::Parameters) | Types::Hash' : '::GiveMeValidType' %> 7 | <% end -%> 8 | option :contract, default: proc { <%= class_name %>Contract.new } 9 | 10 | def call 11 | contract_params = yield validate(contract) 12 | result = yield call_service(contract_params) 13 | 14 | Success(result) 15 | end 16 | 17 | private 18 | 19 | def call_service(contract_params) 20 | <%= class_name %>Service.new(params: contract_params).call 21 | end 22 | end 23 | <% end -%> 24 | -------------------------------------------------------------------------------- /lib/generators/operation/templates/operation_test.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | <% module_namespacing do -%> 6 | class <%= class_name %>OperationTest < ActiveSupport::TestCase 7 | test 'should pass contract validation then calling the service' do 8 | params_mock = mock 9 | params_mock.expects(:to_h).returns(params_mock) 10 | 11 | contract_mock = mock 12 | <%= class_name %>Contract.expects(:new).returns(contract_mock) 13 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 14 | contract_mock.expects(:success?).returns(true) 15 | contract_mock.expects(:to_h).returns(contract_mock) 16 | 17 | service_mock = mock 18 | <%= class_name %>Service.expects(:new).returns(service_mock) 19 | service_mock.expects(:call).returns(Dry::Monads::Result::Success.new(true)) 20 | 21 | operation = <%= class_name %>Operation.new(params: params_mock).call 22 | 23 | assert operation.success? 24 | end 25 | 26 | test 'should return errors if something goes wrong while validating params' do 27 | service_mock = mock 28 | <%= class_name %>Service.expects(:new).returns(service_mock).never 29 | 30 | operation = <%= class_name %>Operation.new(params: {}).call 31 | 32 | errors = contract_errors_parser(operation.failure) 33 | 34 | assert operation.failure? 35 | assert errors.keys.any? 36 | end 37 | 38 | test 'should return errors if something goes wrong while executing service' do 39 | params_mock = mock 40 | params_mock.expects(:to_h).returns(params_mock) 41 | 42 | contract_mock = mock 43 | <%= class_name %>Contract.expects(:new).returns(contract_mock) 44 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 45 | contract_mock.expects(:success?).returns(true) 46 | contract_mock.expects(:to_h).returns(contract_mock) 47 | 48 | service_mock = mock 49 | <%= class_name %>Service.expects(:new).returns(service_mock) 50 | service_mock.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 51 | 52 | operation = <%= class_name %>Operation.new(params: params_mock).call 53 | 54 | assert operation.failure? 55 | assert_equal :failed_because_of_me, operation.failure 56 | end 57 | end 58 | <% end -%> 59 | -------------------------------------------------------------------------------- /lib/generators/service/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates a new service 3 | 4 | Example: 5 | bin/rails generate service Facebook::Ad current_user params 6 | 7 | This will create: 8 | app/services/facebook/ad/create_service.rb 9 | test/services/facebook/ad/create_service_test.rb 10 | -------------------------------------------------------------------------------- /lib/generators/service/service_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ServiceGenerator < Rails::Generators::NamedBase 4 | argument :params, type: :array, default: [], banner: 'action action' 5 | class_option :parent, type: :string, default: 'ApplicationService', desc: 'The parent class for the generated service' 6 | 7 | check_class_collision suffix: 'Service' 8 | 9 | source_root File.expand_path('templates', __dir__) 10 | 11 | def create_service_files 12 | template 'service.rb', File.join('app/services/', class_path, "#{file_name}_service.rb") 13 | template 'service_test.rb', File.join('test/services/', class_path, "#{file_name}_service_test.rb") 14 | 15 | return unless Rails.env.development? 16 | 17 | system("rubocop -A #{service_file_path}") 18 | system("rubocop -A #{service_test_file_path}") 19 | end 20 | 21 | private 22 | 23 | def service_file_path 24 | "app/services/#{class_path.join('/')}/#{file_name}_service.rb" 25 | end 26 | 27 | def service_test_file_path 28 | "test/services/#{class_path.join('/')}/#{file_name}_service_test.rb" 29 | end 30 | 31 | def parent_class_name 32 | options[:parent] 33 | end 34 | 35 | def file_name 36 | @_file_name ||= remove_possible_suffix(super) 37 | end 38 | 39 | def remove_possible_suffix(name) 40 | name.sub(/_?service$/i, '') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/generators/service/templates/service.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | <% module_namespacing do -%> 4 | class <%= class_name %>Service < <%= parent_class_name.classify %> 5 | <% params.each do |param| -%> 6 | option :<%= param %>, type: <%= param == 'params' ? 'Types::Hash' : 'Types::GiveMeValidType' %> 7 | <% end -%> 8 | 9 | def call 10 | result = yield call_something 11 | 12 | Success(result) 13 | end 14 | 15 | private 16 | 17 | def call_something 18 | # must be return Success or Failure 19 | 20 | # call some service ex: Foo::Bar.new(params).call 21 | # or manual ex: Success(Record.all) 22 | end 23 | end 24 | <% end -%> 25 | -------------------------------------------------------------------------------- /lib/generators/service/templates/service_test.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | <% module_namespacing do -%> 6 | class <%= class_name %>ServiceTest < ActiveSupport::TestCase 7 | def setup; end 8 | 9 | test 'should ...' do 10 | assert_changes -> { something } do 11 | service = <%= class_name %>Service.new.call 12 | 13 | assert service.success? 14 | end 15 | end 16 | 17 | test 'should ...' do 18 | assert_no_changes -> { something } do 19 | service = <%= class_name %>Service.new.call 20 | 21 | assert service.failure? 22 | end 23 | end 24 | end 25 | <% end -%> 26 | -------------------------------------------------------------------------------- /lib/supports/application_contract/error_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module ApplicationContract 5 | module ErrorParser 6 | TEST_KEYS_SPLITTER = '_of_' 7 | 8 | def contract_error_parser(contract_result, contract) 9 | errors = contract_result.errors.to_h 10 | total_errors = [] 11 | 12 | errors.each do |key, val| 13 | total_errors.push(extracts_errors(contract, key, val)) 14 | end 15 | 16 | { errors: total_errors.flatten } 17 | end 18 | 19 | private 20 | 21 | def extracts_errors(contract, key, val, top_keys = []) 22 | return handle_error(top_keys.concat([key]), val.first) if val.is_a?(Array) 23 | 24 | val.map do |k, v| 25 | extracts_errors(contract, k, v, top_keys.concat([key])) 26 | end 27 | end 28 | 29 | def handle_error(keys, val) 30 | if Rails.env.test? 31 | { 32 | "#{keys.uniq.reverse.join(TEST_KEYS_SPLITTER).to_sym}": val 33 | } 34 | else 35 | localize_error_message(keys, val) 36 | end 37 | end 38 | 39 | def localize_error_message(keys, val) 40 | case I18n.locale 41 | when :tr 42 | "#{localize_params(keys).reverse.join(keys_splitter)} #{val}" 43 | when :en 44 | "#{localize_params(keys).join(keys_splitter)} #{val}" 45 | else 46 | raise "Unsupported locale #{I18n.locale}" 47 | end 48 | end 49 | 50 | def localize_params(keys) 51 | case I18n.locale 52 | when :tr 53 | keys.map { |k| I18n.t("contracts.params.#{k}").capitalize } 54 | when :en 55 | keys.map(&:capitalize) 56 | else 57 | raise "Unsupported locale #{I18n.locale}" 58 | end 59 | end 60 | 61 | def format_contract_name(contract) 62 | contract.class.name.parameterize.tr('-', '.') 63 | end 64 | 65 | def keys_splitter(locale = I18n.locale) 66 | I18n.t('dry_validation.keys_splitter', locale:) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/supports/application_contract/i18n.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module ApplicationContract 5 | module I18n 6 | # rubocop:disable Rails/Output 7 | def check_i18n_translations_for(errors, locale) 8 | params = errors.keys.map { |k| k.to_s.split(Supports::ApplicationContract::ErrorParser::TEST_KEYS_SPLITTER) }.flatten 9 | 10 | params.each do |param| 11 | translation = ::I18n.t("contracts.params.#{param}", locale:) 12 | ap(translation, { color: { string: :red } }) if translation.include?('translation missing') 13 | end 14 | end 15 | # rubocop:enable Rails/Output 16 | 17 | def check_i18n_translations(errors) 18 | ::I18n.available_locales.each do |locale| 19 | next if locale == :en 20 | 21 | check_i18n_translations_for(errors, locale) 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/supports/application_operation/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module ApplicationOperation 5 | module Helper 6 | def validate(contract) 7 | result = contract.call(params.to_h) 8 | 9 | return contract_success(result) if result.success? 10 | 11 | contract_failure(result, contract) 12 | end 13 | 14 | def contract_failure(result, contract) 15 | Failure(contract_error_parser(result, contract)) 16 | end 17 | 18 | def contract_success(result) 19 | Success(result.to_h) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/supports/application_service/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module ApplicationService 5 | module Helper 6 | def resource_failure(resource) 7 | custom_resource_failure(resource.errors.full_messages) 8 | end 9 | 10 | def custom_resource_failure(errors) 11 | Failure({ errors: }) 12 | end 13 | 14 | def create_resource(klass) 15 | record = klass.new(params) 16 | 17 | return Success(record) if record.save 18 | 19 | resource_failure(record) 20 | end 21 | 22 | def update_resource(record) 23 | return Success(record) if record.update(params) 24 | 25 | resource_failure(resource) 26 | end 27 | 28 | def destroy_resource(record) 29 | return Success(record) if record.destroy 30 | 31 | resource_failure(resource) 32 | end 33 | 34 | def find_resource(klass:, error_message: nil, **args) 35 | record = klass.find_by(args) 36 | 37 | return Success(record) if record.present? 38 | 39 | message = error_message || I18n.t('application_service.default.find_resource.error', klass: klass.to_s) 40 | 41 | custom_resource_failure([message]) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/supports/doorkeeper/custom_error_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/doorkeeper-gem/doorkeeper/blob/main/lib/doorkeeper/oauth/error_response.rb 4 | module Supports 5 | module Doorkeeper 6 | module CustomErrorResponse 7 | def body 8 | { 9 | errors: custom_errors 10 | } 11 | end 12 | 13 | def status 14 | if @error.name == :invalid_client || @error.name == :unauthorized_client 15 | :unauthorized 16 | else 17 | :bad_request 18 | end 19 | end 20 | 21 | private 22 | 23 | def custom_errors 24 | [ 25 | if @error.name == :invalid_client || @error.name == :unauthorized_client 26 | I18n.t('doorkeeper.errors.messages.invalid_client') 27 | else 28 | I18n.t('devise.failure.invalid', authentication_keys: User.authentication_keys.join('/')) 29 | end 30 | ] 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/supports/doorkeeper/custom_register_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/doorkeeper-gem/doorkeeper/blob/main/lib/doorkeeper/oauth/token_response.rb 4 | module Supports 5 | module Doorkeeper 6 | module CustomRegisterResponse 7 | def body(user, access_token, token_type = 'Bearer') 8 | { 9 | user: { 10 | id: user.id, 11 | type: user.class.name, 12 | email: user.email 13 | }, 14 | access_token: access_token.token, 15 | token_type:, 16 | expires_in: access_token.expires_in, 17 | refresh_token: access_token.refresh_token, 18 | created_at: access_token.created_at.to_time.to_i 19 | } 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/supports/doorkeeper/custom_token_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/doorkeeper-gem/doorkeeper/blob/main/lib/doorkeeper/oauth/token_response.rb 4 | module Supports 5 | module Doorkeeper 6 | module CustomTokenResponse 7 | def body 8 | current_user = @token.resource_owner 9 | 10 | additional_data = { 11 | user: { 12 | id: current_user.id, 13 | type: current_user.class.name, 14 | email: current_user.email 15 | } 16 | } 17 | 18 | super.merge(additional_data) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/supports/sidekiq/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # https://github.com/doorkeeper-gem/doorkeeper/blob/main/lib/doorkeeper/oauth/token_response.rb 4 | module Supports 5 | module Sidekiq 6 | module Helper 7 | def job_params_handler(**params) 8 | data = (params || {}).to_json 9 | 10 | JSON.parse(data) 11 | end 12 | 13 | def updated_resource_payload(newest, oldest) 14 | old_attributes = newest.previous_changes.except(:updated_at).transform_values(&:first) 15 | new_attributes = oldest.attributes.slice(*old_attributes.keys) 16 | 17 | job_params_handler(new_attributes:, old_attributes:) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/lib/tasks/.keep -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/log/.keep -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/storage/.keep -------------------------------------------------------------------------------- /test/channels/application_cable/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase 6 | # test "connects with cookies" do 7 | # cookies.signed[:user_id] = 42 8 | # 9 | # connect 10 | # 11 | # assert_equal connection.user_id, "42" 12 | # end 13 | end 14 | -------------------------------------------------------------------------------- /test/contracts/users/passwords/send_instructions_contract_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class SendInstructionsContractTest < ActiveSupport::TestCase 8 | CONTRACT = Users::Passwords::SendInstructionsContract.new 9 | 10 | def validate(payload = {}) 11 | CONTRACT.call(payload) 12 | end 13 | 14 | test 'validate#email' do 15 | success?(validate({ email: 'test@test.com' }), :email, CONTRACT) 16 | filled?(validate({ email: nil }), :email, CONTRACT) 17 | filled?(validate({ email: '' }), :email, CONTRACT) 18 | format?(validate({ email: 'invalid email' }), :email, CONTRACT) 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/contracts/users/passwords/update_contract_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class UpdateContractTest < ActiveSupport::TestCase 8 | CONTRACT = Users::Passwords::UpdateContract.new 9 | 10 | def validate(payload = {}) 11 | CONTRACT.call(payload) 12 | end 13 | 14 | test 'validate#reset_password_token' do 15 | success?(validate({ reset_password_token: 'token' }), :reset_password_token, CONTRACT) 16 | filled?(validate({ reset_password_token: nil }), :reset_password_token, CONTRACT) 17 | filled?(validate({ reset_password_token: '' }), :reset_password_token, CONTRACT) 18 | str?(validate({ reset_password_token: 1 }), :reset_password_token, CONTRACT) 19 | end 20 | 21 | test 'validate#password' do 22 | success?(validate({ password: '123456', password_confirmation: '123456' }), :password, CONTRACT) 23 | filled?(validate({ password: nil }), :password, CONTRACT) 24 | filled?(validate({ password: '' }), :password, CONTRACT) 25 | min_size?(validate({ password: '12345' }), :password, 6, CONTRACT) 26 | same_password?(validate({ password: '1234566', password_confirmation: '123456' }), :password, CONTRACT) 27 | end 28 | 29 | test 'validate#password_confirmation' do 30 | success?(validate({ password_confirmation: '123456', password: '123456' }), :password_confirmation, CONTRACT) 31 | filled?(validate({ password_confirmation: nil }), :password_confirmation, CONTRACT) 32 | filled?(validate({ password_confirmation: '' }), :password_confirmation, CONTRACT) 33 | min_size?(validate({ password_confirmation: '12345' }), :password_confirmation, 6, CONTRACT) 34 | same_password?(validate({ password_confirmation: '1234566', password: '123456' }), :password_confirmation, CONTRACT) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/contracts/users/registrations/register_contract_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Registrations 7 | class RegisterContractTest < ActiveSupport::TestCase 8 | CONTRACT = Users::Registrations::RegisterContract.new 9 | 10 | def validate(payload = {}) 11 | CONTRACT.call(payload) 12 | end 13 | 14 | test 'validate#email' do 15 | success?(validate({ email: 'test@test.com' }), :email, CONTRACT) 16 | filled?(validate({ email: nil }), :email, CONTRACT) 17 | filled?(validate({ email: '' }), :email, CONTRACT) 18 | format?(validate({ email: 'invalid email' }), :email, CONTRACT) 19 | end 20 | 21 | test 'validate#password' do 22 | success?(validate({ password: '123456' }), :password, CONTRACT) 23 | filled?(validate({ password: nil }), :password, CONTRACT) 24 | filled?(validate({ password: '' }), :password, CONTRACT) 25 | min_size?(validate({ password: '12345' }), :password, 6, CONTRACT) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/controllers/swagger_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class SwaggerControllerTest < ActionDispatch::IntegrationTest 4 | test 'should get swagger ui' do 5 | get swagger_root_url 6 | assert_response :success 7 | end 8 | 9 | test 'should get swagger data [V1]' do 10 | get swagger_v1_data_url 11 | assert_response :success 12 | end 13 | 14 | test 'should get swagger data [V2]' do 15 | get swagger_v2_data_url 16 | assert_response :success 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/controllers/v1/users/passwords_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module V1 6 | module Users 7 | class PasswordsControllerTest < ActionDispatch::IntegrationTest 8 | attr_reader :doorkeeper_application, :user 9 | 10 | def setup 11 | @doorkeeper_application = create(:doorkeeper_application) 12 | @user = create(:user) 13 | end 14 | 15 | test 'create#should send an email then returns an information message' do 16 | params = { email: user.email, 17 | client_id: doorkeeper_application.uid, 18 | client_secret: doorkeeper_application.secret } 19 | 20 | assert_difference('Devise.mailer.deliveries.count') do 21 | post(v1_users_password_url, params:, as: :json) 22 | end 23 | 24 | body = load_body(response) 25 | 26 | assert_equal I18n.t('devise.passwords.send_instructions'), body.message 27 | assert_response :ok 28 | end 29 | 30 | test 'create#should return errors if oauth client id is invalid' do 31 | params = { email: user.email, 32 | client_id: 'invalid', 33 | client_secret: doorkeeper_application.secret } 34 | 35 | assert_no_difference('Devise.mailer.deliveries.count') do 36 | post(v1_users_password_url, params:, as: :json) 37 | end 38 | 39 | assert_equal 1, errors_count(response) 40 | error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 41 | assert_response :unauthorized 42 | end 43 | 44 | test 'create#should return errors if oauth client secret is invalid' do 45 | params = { email: user.email, 46 | client_id: doorkeeper_application.uid, 47 | client_secret: 'invalid' } 48 | 49 | assert_no_difference('Devise.mailer.deliveries.count') do 50 | post(v1_users_password_url, params:, as: :json) 51 | end 52 | 53 | assert_equal 1, errors_count(response) 54 | error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 55 | assert_response :unauthorized 56 | end 57 | 58 | test 'create#should not send an email then returns errors if email was not found' do 59 | params = { email: 'not@registered.com', 60 | client_id: doorkeeper_application.uid, 61 | client_secret: doorkeeper_application.secret } 62 | 63 | assert_no_difference('Devise.mailer.deliveries.count') do 64 | post(v1_users_password_url, params:, as: :json) 65 | end 66 | 67 | error_message?(response, 'Email not found') 68 | assert_equal 1, errors_count(response) 69 | assert_response :unprocessable_entity 70 | end 71 | 72 | test 'update#should update password' do 73 | token = user.send_reset_password_instructions 74 | params = { reset_password_token: token, 75 | password: user.password, 76 | password_confirmation: user.password, 77 | client_id: doorkeeper_application.uid, 78 | client_secret: doorkeeper_application.secret } 79 | 80 | assert_changes -> { user.updated_at } do 81 | patch(v1_users_password_url, params:, as: :json) 82 | 83 | user.reload 84 | end 85 | 86 | body = load_body(response) 87 | 88 | assert_equal I18n.t('devise.passwords.updated_not_active'), body.message 89 | assert_response :ok 90 | end 91 | 92 | test 'update#should return errors if oauth client id is invalid' do 93 | token = user.send_reset_password_instructions 94 | params = { reset_password_token: token, 95 | password: user.password, 96 | password_confirmation: user.password, 97 | client_id: 'invalid', 98 | client_secret: doorkeeper_application.secret } 99 | 100 | assert_no_difference('Devise.mailer.deliveries.count') do 101 | patch(v1_users_password_url, params:, as: :json) 102 | end 103 | 104 | assert_equal 1, errors_count(response) 105 | error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 106 | assert_response :unauthorized 107 | end 108 | 109 | test 'update#should return errors if oauth client secret is invalid' do 110 | token = user.send_reset_password_instructions 111 | params = { reset_password_token: token, 112 | password: user.password, 113 | password_confirmation: user.password, 114 | client_id: doorkeeper_application.uid, 115 | client_secret: 'invalid' } 116 | 117 | assert_no_difference('Devise.mailer.deliveries.count') do 118 | patch(v1_users_password_url, params:, as: :json) 119 | end 120 | 121 | assert_equal 1, errors_count(response) 122 | error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 123 | assert_response :unauthorized 124 | end 125 | 126 | test 'update#should not update password with invalid token' do 127 | user.send_reset_password_instructions 128 | params = { reset_password_token: 'so_secret_token', 129 | password: user.password, 130 | password_confirmation: user.password, 131 | client_id: doorkeeper_application.uid, 132 | client_secret: doorkeeper_application.secret } 133 | 134 | assert_no_changes -> { user.updated_at } do 135 | patch(v1_users_password_url, params:, as: :json) 136 | 137 | user.reload 138 | end 139 | 140 | assert error_message?(response, 'Reset password token is invalid') 141 | assert_equal 1, errors_count(response) 142 | assert_response :unprocessable_entity 143 | end 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /test/controllers/v1/users/registrations_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module V1 6 | module Users 7 | class RegistrationsControllerTest < ActionDispatch::IntegrationTest 8 | attr_reader :doorkeeper_application 9 | 10 | def setup 11 | @doorkeeper_application = create(:doorkeeper_application) 12 | end 13 | 14 | test 'create#should register and then generate access token' do 15 | params = { email: 'tester@mail.com', 16 | password: '123456', 17 | client_id: doorkeeper_application.uid, 18 | client_secret: doorkeeper_application.secret } 19 | 20 | assert_difference(['Doorkeeper::AccessToken.count', 'User.count'], 1) do 21 | post(v1_users_registration_url, params:, as: :json) 22 | end 23 | 24 | body = load_body(response) 25 | 26 | assert_equal params[:email], body.user.email 27 | assert_response :created 28 | end 29 | 30 | test 'create#should not register if email has already been taken' do 31 | old_user = create(:user) 32 | params = { email: old_user.email, 33 | password: '123456', 34 | client_id: doorkeeper_application.uid, 35 | client_secret: doorkeeper_application.secret } 36 | 37 | assert_no_difference(['Doorkeeper::AccessToken.count', 'User.count'], 1) do 38 | post(v1_users_registration_url, params:, as: :json) 39 | end 40 | 41 | assert_equal 1, errors_count(response) 42 | assert error_message?(response, 'Email has already been taken') 43 | assert_response :unprocessable_entity 44 | end 45 | 46 | test 'create#should return errors if oauth client id is invalid' do 47 | params = { email: 'tester@mail.com', 48 | password: '123456', 49 | client_id: 'invalid', 50 | client_secret: doorkeeper_application.secret } 51 | 52 | assert_no_difference(['Doorkeeper::AccessToken.count', 'User.count'], 1) do 53 | post(v1_users_registration_url, params:, as: :json) 54 | end 55 | 56 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 57 | assert_equal 1, errors_count(response) 58 | assert_response :unauthorized 59 | end 60 | 61 | test 'create#should return errors if oauth client secret is invalid' do 62 | params = { email: 'tester@mail.com', 63 | password: '123456', 64 | client_id: doorkeeper_application.uid, 65 | client_secret: 'invalid' } 66 | 67 | assert_no_difference(['Doorkeeper::AccessToken.count', 'User.count'], 1) do 68 | post(v1_users_registration_url, params:, as: :json) 69 | end 70 | 71 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 72 | assert_equal 1, errors_count(response) 73 | assert_response :unauthorized 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/controllers/v1/users/tokens_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module V1 6 | module Users 7 | class TokensControllerTest < ActionDispatch::IntegrationTest 8 | attr_reader :user, :doorkeeper_application, :token 9 | 10 | def setup 11 | @user = create(:user) 12 | @doorkeeper_application = create(:doorkeeper_application) 13 | @token = create(:doorkeeper_access_token, application: @doorkeeper_application, resource_owner: @user) 14 | end 15 | 16 | test 'create#should generate new access and refresh tokens' do 17 | assert_difference('Doorkeeper::AccessToken.count') do 18 | post(v1_users_oauth_token_url, 19 | params: oauth_token_params(user, doorkeeper_application), 20 | as: :json) 21 | end 22 | 23 | body = load_body(response) 24 | 25 | assert_respond_to body, :access_token 26 | assert_respond_to body, :refresh_token 27 | assert_respond_to body, :token_type 28 | assert_respond_to body, :expires_in 29 | assert_respond_to body, :created_at 30 | assert_respond_to body.user, :email 31 | assert_respond_to body.user, :type 32 | assert_respond_to body.user, :id 33 | assert_response :success 34 | end 35 | 36 | test 'create#should fail client id is invalid' do 37 | params = { grant_type: 'password', 38 | email: user.email, 39 | password: user.password, 40 | client_id: 'invalid', 41 | client_secret: doorkeeper_application.secret } 42 | 43 | assert_no_difference('Doorkeeper::AccessToken.count') do 44 | post(v1_users_oauth_token_url, params:, as: :json) 45 | end 46 | 47 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 48 | assert_equal 1, errors_count(response) 49 | assert_response :unauthorized 50 | end 51 | 52 | test 'create#should fail client secret is invalid' do 53 | params = { grant_type: 'password', 54 | email: user.email, 55 | password: user.password, 56 | client_id: doorkeeper_application.uid, 57 | client_secret: 'invalid' } 58 | 59 | assert_no_difference('Doorkeeper::AccessToken.count') do 60 | post(v1_users_oauth_token_url, params:, as: :json) 61 | end 62 | 63 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.invalid_client')) 64 | assert_equal 1, errors_count(response) 65 | assert_response :unauthorized 66 | end 67 | 68 | test 'create#should not generate new access and refresh tokens if user credentials is invalid' do 69 | invalid_user = build(:user) 70 | 71 | assert_no_difference('Doorkeeper::AccessToken.count') do 72 | post(v1_users_oauth_token_url, 73 | params: oauth_token_params(invalid_user, doorkeeper_application), 74 | as: :json) 75 | end 76 | 77 | assert error_message?(response, I18n.t('devise.failure.invalid', authentication_keys: User.authentication_keys.join('/'))) 78 | assert_equal 1, errors_count(response) 79 | assert_response :bad_request 80 | end 81 | 82 | test 'create#should generate new access and refresh tokens with refresh token' do 83 | assert_difference('Doorkeeper::AccessToken.count') do 84 | post(v1_users_oauth_token_url, 85 | params: oauth_refresh_token_params(token), 86 | as: :json) 87 | end 88 | 89 | body = load_body(response) 90 | 91 | assert_respond_to body, :access_token 92 | assert_respond_to body, :refresh_token 93 | assert_respond_to body, :token_type 94 | assert_respond_to body, :expires_in 95 | assert_respond_to body, :created_at 96 | assert_response :success 97 | end 98 | 99 | test 'create#should not generate new access and refresh tokens if refresh token is invalid' do 100 | assert_no_difference('Doorkeeper::AccessToken.count') do 101 | post(v1_users_oauth_token_url, 102 | params: oauth_refresh_token_params(token, 'token')) 103 | end 104 | 105 | assert error_message?(response, I18n.t('devise.failure.invalid', authentication_keys: User.authentication_keys.join('/'))) 106 | assert_equal 1, errors_count(response) 107 | assert_response :bad_request 108 | end 109 | 110 | test 'revoke#should revoke access token' do 111 | assert_changes -> { token.revoked_at } do 112 | post(v1_users_oauth_revoke_url, 113 | params: oauth_revoke_params(token), 114 | as: :json) 115 | 116 | token.reload 117 | end 118 | 119 | assert_response :success 120 | end 121 | 122 | test 'revoke#should not revoke access token if token has already revoked' do 123 | token.revoke 124 | 125 | assert_no_changes -> { token.revoked_at } do 126 | post(v1_users_oauth_revoke_url, params: oauth_revoke_params(token), as: :json) 127 | 128 | token.reload 129 | end 130 | 131 | assert_response :success 132 | end 133 | 134 | test 'revoke#should fail client id is invalid' do 135 | params = { token: token.token, 136 | client_id: 'invalid', 137 | client_secret: doorkeeper_application.secret } 138 | 139 | assert_no_changes -> { token.revoked_at } do 140 | post(v1_users_oauth_revoke_url, params:, as: :json) 141 | 142 | token.reload 143 | end 144 | 145 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.revoke.unauthorized')) 146 | assert_equal 1, errors_count(response) 147 | assert_response :forbidden 148 | end 149 | 150 | test 'revoke#should fail client secret is invalid' do 151 | params = { token: token.token, 152 | client_id: doorkeeper_application.uid, 153 | client_secret: 'invalid' } 154 | 155 | assert_no_changes -> { token.revoked_at } do 156 | post(v1_users_oauth_revoke_url, params:, as: :json) 157 | 158 | token.reload 159 | end 160 | 161 | assert error_message?(response, I18n.t('doorkeeper.errors.messages.revoke.unauthorized')) 162 | assert_equal 1, errors_count(response) 163 | assert_response :forbidden 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /test/factories/doorkeeper/doorkeeper_access_tokens_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :doorkeeper_access_token, class: 'Doorkeeper::AccessToken' do 5 | association :application, factory: :doorkeeper_application 6 | expires_in { 1.hour } 7 | association :resource_owner, factory: :user 8 | sequence :refresh_token do |n| 9 | SecureRandom.hex(32) + n.to_s 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/factories/doorkeeper/doorkeeper_applications_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :doorkeeper_application, class: 'Doorkeeper::Application' do 5 | name { Faker::App.name } 6 | redirect_uri { '' } 7 | scopes { '' } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/factories/users_factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | FactoryBot.define do 4 | factory :user do 5 | email { Faker::Internet.email } 6 | password { Faker::Internet.password(min_length: 6) } 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/files/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/test/fixtures/files/.keep -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/test/integration/.keep -------------------------------------------------------------------------------- /test/lib/custom_objects/parameter_object/pagination_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module CustomObjects 6 | module ParameterObject 7 | class PaginationTest < ActiveSupport::TestCase 8 | test 'default values' do 9 | pagination = CustomObjects::ParameterObject::Pagination.new 10 | 11 | assert_equal 1, pagination.page 12 | assert_equal 25, pagination.per_page 13 | end 14 | 15 | test 'page' do 16 | pagination = CustomObjects::ParameterObject::Pagination.new(page: 2) 17 | pagination_coercible = CustomObjects::ParameterObject::Pagination.new(page: '2') 18 | 19 | assert_equal 2, pagination.page 20 | assert_equal 2, pagination_coercible.page 21 | end 22 | 23 | test 'per page' do 24 | pagination = CustomObjects::ParameterObject::Pagination.new(per_page: 2) 25 | pagination_coercible = CustomObjects::ParameterObject::Pagination.new(per_page: '2') 26 | 27 | assert_equal 2, pagination.per_page 28 | assert_equal 2, pagination_coercible.per_page 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/lib/custom_objects/parameter_object/query_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module CustomObjects 6 | module ParameterObject 7 | class QueryTest < ActiveSupport::TestCase 8 | test 'default values' do 9 | query = CustomObjects::ParameterObject::Query.new 10 | 11 | assert_empty query.query 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/lib/generators/contract_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'generators/contract/contract_generator' 5 | 6 | class ContractGeneratorTest < Rails::Generators::TestCase 7 | tests ContractGenerator 8 | destination Rails.root.join('tmp/generators') 9 | setup :prepare_destination 10 | 11 | test 'generator runs without errors' do 12 | assert_nothing_raised do 13 | run_generator ['Customer'] 14 | end 15 | end 16 | 17 | test 'generator creates contract' do 18 | run_generator ['Customer::Create'] 19 | 20 | assert_file 'app/contracts/customer/create_contract.rb' 21 | assert_file 'test/contracts/customer/create_contract_test.rb' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/lib/generators/operation_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'generators/operation/operation_generator' 5 | 6 | class OperationGeneratorTest < Rails::Generators::TestCase 7 | tests OperationGenerator 8 | destination Rails.root.join('tmp/generators') 9 | setup :prepare_destination 10 | 11 | test 'generator runs without errors' do 12 | assert_nothing_raised do 13 | run_generator ['Seller'] 14 | end 15 | end 16 | 17 | test 'generator creates operation files' do 18 | run_generator ['Seller::Create'] 19 | 20 | assert_file 'app/operations/seller/create_operation.rb' 21 | assert_file 'test/operations/seller/create_operation_test.rb' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/lib/generators/service_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'generators/service/service_generator' 5 | 6 | class ServiceGeneratorTest < Rails::Generators::TestCase 7 | tests ServiceGenerator 8 | destination Rails.root.join('tmp/generators') 9 | setup :prepare_destination 10 | 11 | test 'generator runs without errors' do 12 | assert_nothing_raised do 13 | run_generator ['Seller'] 14 | end 15 | end 16 | 17 | test 'generator creates service files' do 18 | run_generator ['Seller::Create'] 19 | 20 | assert_file 'app/services/seller/create_service.rb' 21 | assert_file 'test/services/seller/create_service_test.rb' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/user_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class UserTest < ActiveSupport::TestCase 6 | test 'should have a valid factory' do 7 | assert create(:user).persisted? 8 | end 9 | 10 | context '#indexes' do 11 | should have_db_index(:email).unique(true) 12 | should have_db_index(:reset_password_token).unique(true) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/operations/users/passwords/create_operation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class CreateOperationTest < ActiveSupport::TestCase 8 | test 'should pass contract validation then calling the service' do 9 | params_mock = mock 10 | params_mock.expects(:to_h).returns(params_mock) 11 | 12 | contract_mock = mock 13 | Users::Passwords::SendInstructionsContract.expects(:new).returns(contract_mock) 14 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 15 | contract_mock.expects(:success?).returns(true) 16 | contract_mock.expects(:to_h).returns(contract_mock) 17 | 18 | service_mock = mock 19 | Users::Passwords::SendInstructionsService.expects(:new).returns(service_mock) 20 | service_mock.expects(:call).returns(Dry::Monads::Result::Success.new(true)) 21 | 22 | operation = Users::Passwords::CreateOperation.new(params: params_mock).call 23 | 24 | assert operation.success? 25 | end 26 | 27 | test 'should return errors if something goes wrong while validating params' do 28 | service_mock = mock 29 | Users::Passwords::SendInstructionsService.expects(:new).returns(service_mock).never 30 | 31 | operation = Users::Passwords::CreateOperation.new(params: {}).call 32 | 33 | errors = contract_errors_parser(operation.failure) 34 | 35 | assert operation.failure? 36 | assert_equal errors[:email], 'is missing' 37 | end 38 | 39 | test 'should return errors if something goes wrong while executing service' do 40 | params_mock = mock 41 | params_mock.expects(:to_h).returns(params_mock) 42 | 43 | contract_mock = mock 44 | Users::Passwords::SendInstructionsContract.expects(:new).returns(contract_mock) 45 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 46 | contract_mock.expects(:success?).returns(true) 47 | contract_mock.expects(:to_h).returns(contract_mock) 48 | 49 | service_mock = mock 50 | Users::Passwords::SendInstructionsService.expects(:new).returns(service_mock) 51 | service_mock.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 52 | 53 | operation = Users::Passwords::CreateOperation.new(params: params_mock).call 54 | 55 | assert operation.failure? 56 | assert_equal :failed_because_of_me, operation.failure 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/operations/users/passwords/update_operation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class UpdateOperationTest < ActiveSupport::TestCase 8 | test 'should pass contract validation then calling the service' do 9 | params_mock = mock 10 | params_mock.expects(:to_h).returns(params_mock) 11 | 12 | contract_mock = mock 13 | Users::Passwords::UpdateContract.expects(:new).returns(contract_mock) 14 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 15 | contract_mock.expects(:success?).returns(true) 16 | contract_mock.expects(:to_h).returns(contract_mock) 17 | 18 | service_mock = mock 19 | Users::Passwords::UpdateService.expects(:new).returns(service_mock) 20 | service_mock.expects(:call).returns(Dry::Monads::Result::Success.new(true)) 21 | 22 | operation = Users::Passwords::UpdateOperation.new(params: params_mock).call 23 | 24 | assert operation.success? 25 | end 26 | 27 | test 'should return errors if something goes wrong while validating params' do 28 | service_mock = mock 29 | Users::Passwords::UpdateService.expects(:new).returns(service_mock).never 30 | 31 | operation = Users::Passwords::UpdateOperation.new(params: {}).call 32 | 33 | errors = contract_errors_parser(operation.failure) 34 | 35 | assert operation.failure? 36 | assert_equal errors[:password], 'is missing' 37 | assert_equal errors[:password_confirmation], 'is missing' 38 | end 39 | 40 | test 'should return errors if something goes wrong while executing service' do 41 | params_mock = mock 42 | params_mock.expects(:to_h).returns(params_mock) 43 | 44 | contract_mock = mock 45 | Users::Passwords::UpdateContract.expects(:new).returns(contract_mock) 46 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 47 | contract_mock.expects(:success?).returns(true) 48 | contract_mock.expects(:to_h).returns(contract_mock) 49 | 50 | service_mock = mock 51 | Users::Passwords::UpdateService.expects(:new).returns(service_mock) 52 | service_mock.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 53 | 54 | operation = Users::Passwords::UpdateOperation.new(params: params_mock).call 55 | 56 | assert operation.failure? 57 | assert_equal :failed_because_of_me, operation.failure 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/operations/users/registrations/create_operation_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Registrations 7 | class CreateOperationTest < ActiveSupport::TestCase 8 | attr_reader :doorkeeper_application 9 | 10 | def setup 11 | @doorkeeper_application = create(:doorkeeper_application) 12 | end 13 | 14 | test 'should pass contract validation then calling the service' do 15 | params_mock = mock 16 | params_mock.expects(:to_h).returns(params_mock) 17 | 18 | contract_mock = mock 19 | Users::Registrations::RegisterContract.expects(:new).returns(contract_mock) 20 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 21 | contract_mock.expects(:success?).returns(true) 22 | contract_mock.expects(:to_h).returns(contract_mock) 23 | 24 | service_mock = mock 25 | Users::Registrations::RegisterService.expects(:new).returns(service_mock) 26 | service_mock.expects(:call).returns(Dry::Monads::Result::Success.new(true)) 27 | 28 | operation = Users::Registrations::CreateOperation.new(params: params_mock, doorkeeper_application:).call 29 | 30 | assert operation.success? 31 | end 32 | 33 | test 'should return errors if something goes wrong while validating params' do 34 | service_mock = mock 35 | Users::Registrations::RegisterService.expects(:new).returns(service_mock).never 36 | 37 | operation = Users::Registrations::CreateOperation.new(params: {}, doorkeeper_application:).call 38 | 39 | errors = contract_errors_parser(operation.failure) 40 | 41 | assert operation.failure? 42 | assert_equal errors[:email], 'is missing' 43 | assert_equal errors[:password], 'is missing' 44 | end 45 | 46 | test 'should return errors if something goes wrong while executing service' do 47 | params_mock = mock 48 | params_mock.expects(:to_h).returns(params_mock) 49 | 50 | contract_mock = mock 51 | Users::Registrations::RegisterContract.expects(:new).returns(contract_mock) 52 | contract_mock.expects(:call).with(params_mock).returns(contract_mock) 53 | contract_mock.expects(:success?).returns(true) 54 | contract_mock.expects(:to_h).returns(contract_mock) 55 | 56 | service_mock = mock 57 | Users::Registrations::RegisterService.expects(:new).returns(service_mock) 58 | service_mock.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 59 | 60 | operation = Users::Registrations::CreateOperation.new(params: params_mock, doorkeeper_application:).call 61 | 62 | assert operation.failure? 63 | assert_equal :failed_because_of_me, operation.failure 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/services/doorkeeper/access_tokens/create_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Doorkeeper 6 | module AccessTokens 7 | class CreateServiceTest < ActiveSupport::TestCase 8 | attr_reader :doorkeeper_application, :user 9 | 10 | def setup 11 | @doorkeeper_application = create(:doorkeeper_application) 12 | @user = create(:user) 13 | end 14 | 15 | test 'should create doorkeeper access token' do 16 | assert_difference 'Doorkeeper::AccessToken.count', 1 do 17 | service = Doorkeeper::AccessTokens::CreateService.new(doorkeeper_application:, user:).call 18 | 19 | assert service.success? 20 | end 21 | end 22 | 23 | test 'should fail if something goes wrong' do 24 | Doorkeeper::AccessToken.any_instance.expects(:save).returns(false) 25 | 26 | assert_no_difference 'Doorkeeper::AccessToken.count' do 27 | service = Doorkeeper::AccessTokens::CreateService.new(doorkeeper_application:, user:).call 28 | 29 | assert service.failure? 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/services/users/create_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | class CreateServiceTest < ActiveSupport::TestCase 7 | attr_reader :params 8 | 9 | def setup 10 | @params = attributes_for(:user) 11 | end 12 | 13 | test 'should create user' do 14 | assert_difference 'User.count', 1 do 15 | service = Users::CreateService.new(params:).call 16 | 17 | assert service.success? 18 | end 19 | end 20 | 21 | test 'should fail if something goes wrong' do 22 | User.any_instance.expects(:save).returns(false) 23 | 24 | assert_no_difference 'User.count' do 25 | service = Users::CreateService.new(params:).call 26 | 27 | assert service.failure? 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/services/users/passwords/send_instructions_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class SendInstructionsServiceTest < ActiveSupport::TestCase 8 | attr_reader :user 9 | 10 | def setup 11 | @user = create(:user) 12 | end 13 | 14 | test 'should send instructions to user' do 15 | params = { email: user.email } 16 | 17 | assert_changes -> { user.updated_at } do 18 | service = Users::Passwords::SendInstructionsService.new(params:).call 19 | 20 | user.reload 21 | 22 | assert service.success? 23 | assert_equal I18n.t('devise.passwords.send_instructions'), service.success[:message] 24 | end 25 | end 26 | 27 | test 'should not update password if token is invalid' do 28 | params = { email: 'test@development.com' } 29 | 30 | assert_no_changes -> { user.updated_at } do 31 | service = Users::Passwords::SendInstructionsService.new(params:).call 32 | 33 | user.reload 34 | 35 | assert service.failure? 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/services/users/passwords/update_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Passwords 7 | class UpdateServiceTest < ActiveSupport::TestCase 8 | attr_reader :user 9 | 10 | def setup 11 | @user = create(:user) 12 | end 13 | 14 | test 'should update password successfully' do 15 | reset_password_token = user.send_reset_password_instructions 16 | 17 | params = { reset_password_token:, 18 | password: '123456', 19 | password_confirmation: '123456' } 20 | 21 | assert_changes -> { user.updated_at } do 22 | service = Users::Passwords::UpdateService.new(params:).call 23 | 24 | user.reload 25 | 26 | assert service.success? 27 | assert_equal I18n.t('devise.passwords.updated_not_active'), service.success[:message] 28 | end 29 | end 30 | 31 | test 'should not update password if token is invalid' do 32 | params = { reset_password_token: 'invalid', 33 | password: '123456', 34 | password_confirmation: '123456' } 35 | 36 | assert_no_changes -> { user.updated_at } do 37 | service = Users::Passwords::UpdateService.new(params:).call 38 | 39 | user.reload 40 | 41 | assert service.failure? 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/services/users/registrations/register_service_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module Users 6 | module Registrations 7 | class RegisterServiceTest < ActiveSupport::TestCase 8 | attr_reader :params, :doorkeeper_application 9 | 10 | def setup 11 | @params = attributes_for(:user) 12 | @doorkeeper_application = create(:doorkeeper_application) 13 | end 14 | 15 | test 'should register user' do 16 | assert_difference(['User.count', 'Doorkeeper::AccessToken.count'], 1) do 17 | service = Users::Registrations::RegisterService.new(params:, doorkeeper_application:).call 18 | 19 | user = service.success[:user] 20 | 21 | assert service.success? 22 | assert_equal params[:email], user[:email] 23 | end 24 | end 25 | 26 | test 'should return error if something goes wrong while creating doorkeeper access token' do 27 | access_token_service_mock = mock 28 | Doorkeeper::AccessTokens::CreateService.expects(:new).returns(access_token_service_mock) 29 | access_token_service_mock.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 30 | 31 | assert_no_difference(['User.count', 'Doorkeeper::AccessToken.count']) do 32 | service = Users::Registrations::RegisterService.new(params:, doorkeeper_application:).call 33 | 34 | assert service.failure? 35 | assert_equal :failed_because_of_me, service.failure 36 | end 37 | end 38 | 39 | test 'should fail if user service returns failure' do 40 | user_service = mock 41 | Users::CreateService.expects(:new).returns(user_service) 42 | user_service.expects(:call).returns(Dry::Monads::Result::Failure.new(:failed_because_of_me)) 43 | 44 | assert_no_difference(['User.count', 'Doorkeeper::AccessToken.count']) do 45 | service = Users::Registrations::RegisterService.new(params:, doorkeeper_application:).call 46 | 47 | assert service.failure? 48 | assert_equal :failed_because_of_me, service.failure 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/supports/body_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module BodyParser 5 | def load_body(response) 6 | JSON.parse(response.body, object_class: OpenStruct) 7 | end 8 | 9 | def errors_count(response) 10 | load_body(response).errors.count 11 | end 12 | 13 | def error_message?(response, message) 14 | load_body(response).errors.include?(message) 15 | end 16 | 17 | def parse_meta(response) 18 | body = load_body(response) 19 | 20 | body.meta 21 | end 22 | 23 | def pagination_present?(response) 24 | pagination = parse_meta(response).pagination 25 | expected_keys = %i[current previous next limit total_pages total_count] 26 | 27 | expected_keys == pagination.keys 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/supports/contract_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module ContractParser 5 | def contract_errors_parser(contract_result) 6 | errors = {} 7 | 8 | contract_result.fetch(:errors, []).each do |err| 9 | k = err.keys.first 10 | v = err.values.first 11 | errors[k] = v 12 | end 13 | 14 | errors 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/supports/doorkeeper_authenticator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module DoorkeeperAuthenticator 5 | def oauth_token_params(user, application, grant_type = ::Doorkeeper.config.grant_flows.first) 6 | { 7 | grant_type:, 8 | email: user.email, 9 | password: user.password, 10 | client_id: application.uid, 11 | client_secret: application.secret 12 | } 13 | end 14 | 15 | def oauth_revoke_params(token, token_type = 'token', grant_type = 'refresh_token') 16 | { 17 | grant_type:, 18 | token: token.try(token_type), 19 | client_id: token.application.uid, 20 | client_secret: token.application.secret 21 | } 22 | end 23 | 24 | def oauth_refresh_token_params(token, token_type = 'refresh_token') 25 | { 26 | grant_type: 'refresh_token', 27 | refresh_token: token.try(token_type), 28 | client_id: token.application.uid, 29 | client_secret: token.application.secret 30 | } 31 | end 32 | 33 | def authorization_header(token) 34 | { 'Authorization' => "Bearer #{token.token}" } 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/supports/sidekiq_minitest_support.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Supports 4 | module SidekiqMinitestSupport 5 | def after_teardown 6 | ::Sidekiq::Worker.clear_all 7 | super 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RAILS_ENV'] ||= 'test' 4 | require_relative '../config/environment' 5 | require 'rails/test_help' 6 | require 'shoulda/matchers' 7 | require 'minitest/unit' 8 | require 'mocha/minitest' 9 | require 'minitest/focus' 10 | require 'supports/contract_validator' 11 | require 'supports/body_parser' 12 | require 'supports/contract_parser' 13 | require 'supports/doorkeeper_authenticator' 14 | require 'supports/sidekiq_minitest_support' 15 | require 'sidekiq/testing' 16 | 17 | class ActiveSupport::TestCase 18 | # Run tests in parallel with specified workers 19 | # parallelize(workers: :number_of_processors) 20 | 21 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 22 | # fixtures :all 23 | 24 | include FactoryBot::Syntax::Methods 25 | include Supports::BodyParser 26 | include Supports::ContractParser 27 | include Supports::ContractValidator 28 | include Supports::DoorkeeperAuthenticator 29 | include Supports::SidekiqMinitestSupport 30 | end 31 | 32 | Shoulda::Matchers.configure do |config| 33 | config.integrate do |with| 34 | with.test_framework :minitest 35 | with.library :rails 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/tmp/.keep -------------------------------------------------------------------------------- /tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/tmp/pids/.keep -------------------------------------------------------------------------------- /tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/tmp/storage/.keep -------------------------------------------------------------------------------- /vendor/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shftco/rails-api-boilerplate/dcaa654b4b40c0a38a6989213cc22ea575ece65a/vendor/.keep --------------------------------------------------------------------------------