├── README.md ├── template.rb └── templates ├── .gitignore ├── app ├── controllers │ ├── api │ │ ├── base_controller.rb │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── registrations_controller.rb │ └── sessions_controller.rb ├── models │ ├── jwt_denylist.rb │ └── user.rb └── serializable │ └── SerializableUser.rb ├── config ├── initializers │ ├── devise.rb │ └── rack_cors.rb └── routes.rb └── spec ├── controllers ├── registrations_controller_spec.rb ├── sessions_controller_spec.rb └── users_controller_spec.rb ├── factories └── users.rb ├── rails_helper.rb └── support ├── api_helpers.rb └── user_helpers.rb /README.md: -------------------------------------------------------------------------------- 1 | # 😴 Boring Bits: Rails API Edition 2 | 3 | **A bare-bones Rails 6 template for API-only applications which use JWT authentication.** 4 | 5 | It's not glamourous. It's not sexy. It's just a dependably Boring starting point for your next API. 6 | 7 | Plays nicely with [Boring Bits: Nextjs Edition](https://github.com/jameschambers/boring-bits-nextjs). 8 | 9 | ## Highlights 10 | 11 | - Devise auth with JSON Web Tokens (JWT) 12 | - Login, signup, and protected route examples 13 | - JSON:API response rendering 14 | - Namespaced /api/ routes 15 | - TDD-ready with passing Rspec tests 16 | 17 | ## Getting started 18 | 19 | Ensure you have Ruby 2.6.x and Rails 6 installed on your machine. 20 | 21 | Clone this repository, then specify your local path to `template.rb` with the `-m` flag: 22 | 23 | ``` 24 | $ rails new YourAppName --api -m ../local/path/to/template.rb 25 | ``` 26 | 27 | The template will install all necessary gems, and create a new database for you. Check everything is installed correctly by cd-ing into `YourAppName`, then running the specs with: 28 | 29 | ``` 30 | $ foreman run rake 31 | ``` 32 | 33 | ## Making API calls 34 | 35 | API calls are authenticated with JWT tokens taken from the `Authorization` header. See `spec/controllers/user_controller_spec.rb` for an example. 36 | 37 | `Authorization` headers are returned by login and signup routes, so you need to save the header value client-side, then include that header with your subsequent API requests. 38 | 39 | -------------------------------------------------------------------------------- /template.rb: -------------------------------------------------------------------------------- 1 | def source_paths 2 | Array(super) + [File.expand_path(File.dirname(__FILE__))] 3 | end 4 | 5 | def copy_and_replace(source, dest = nil) 6 | dest_file = dest.nil? ? source : dest 7 | copy_file("templates/#{source}", dest_file, force: true) 8 | end 9 | 10 | remove_file 'Gemfile' 11 | run 'touch Gemfile' 12 | add_source 'https://rubygems.org' 13 | 14 | # ruby '2.6.0' 15 | 16 | gem 'bootsnap', '>= 1.4.2', require: false 17 | gem 'devise' 18 | gem 'devise-jwt', '~> 0.7.0' 19 | gem 'foreman' 20 | gem 'jsonapi-rails' 21 | gem 'pg', '>= 0.18', '< 2.0' 22 | gem 'puma', '~> 4.1' 23 | gem 'rack-cors' 24 | gem 'rails', '~> 6.0.3', '>= 6.0.3.2' 25 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 26 | 27 | gem_group :development, :test do 28 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 29 | gem 'rspec-rails', '~> 3.8' 30 | gem 'factory_bot_rails' 31 | end 32 | 33 | gem_group :development do 34 | gem 'listen', '~> 3.2' 35 | gem 'rubocop' 36 | gem 'solargraph' 37 | gem 'spring' 38 | gem 'spring-watcher-listen', '~> 2.0.0' 39 | gem 'web-console', '>= 3.3.0' 40 | end 41 | 42 | gem_group :test do 43 | gem 'capybara', '>= 2.15' 44 | gem 'faker' 45 | gem 'selenium-webdriver' 46 | gem 'shoulda-matchers', '~> 4.0' 47 | gem 'vcr' 48 | gem 'webdrivers' 49 | gem 'jsonapi-rspec' 50 | gem 'webmock' 51 | end 52 | 53 | inside 'config' do 54 | remove_file 'database.yml' 55 | create_file 'database.yml' do <<-EOF 56 | default: &default 57 | adapter: postgresql 58 | encoding: unicode 59 | # For details on connection pooling, see Rails configuration guide 60 | # https://guides.rubyonrails.org/configuring.html#database-pooling 61 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 62 | 63 | development: 64 | <<: *default 65 | database: #{app_name}_development 66 | 67 | test: 68 | <<: *default 69 | database: #{app_name}_test 70 | 71 | production: 72 | <<: *default 73 | database: #{app_name}_production 74 | 75 | EOF 76 | end 77 | end 78 | 79 | ## -------------------------------------------------- 80 | ## Installers 81 | ## -------------------------------------------------- 82 | 83 | run 'bundle install' 84 | 85 | generate('devise:install') 86 | generate('devise User') 87 | generate('rspec:install') 88 | 89 | ## -------------------------------------------------- 90 | ## DB migrations 91 | ## -------------------------------------------------- 92 | 93 | generate('devise:install') 94 | generate('migration', 'CreateJwtDenylist jti:string:index expired_at:datetime') 95 | 96 | ## -------------------------------------------------- 97 | ## Create databases 98 | ## -------------------------------------------------- 99 | 100 | rails_command('db:create') 101 | rails_command('db:migrate') 102 | 103 | ## -------------------------------------------------- 104 | ## Create files from templates 105 | ## -------------------------------------------------- 106 | 107 | copy_and_replace '.gitignore' 108 | 109 | ## Controllers 110 | copy_and_replace 'app/controllers/application_controller.rb' 111 | copy_and_replace 'app/controllers/registrations_controller.rb' 112 | copy_and_replace 'app/controllers/sessions_controller.rb' 113 | copy_and_replace 'app/controllers/api/base_controller.rb' 114 | copy_and_replace 'app/controllers/api/users_controller.rb' 115 | 116 | # Models 117 | copy_and_replace 'app/models/user.rb' 118 | copy_and_replace 'app/models/jwt_denylist.rb' 119 | 120 | # Serializer 121 | copy_and_replace 'app/serializable/SerializableUser.rb' 122 | 123 | # Config 124 | copy_and_replace 'config/initializers/devise.rb' 125 | copy_and_replace 'config/routes.rb' 126 | copy_and_replace 'config/initializers/rack_cors.rb' 127 | 128 | # Spec 129 | copy_and_replace 'spec/rails_helper.rb' 130 | copy_and_replace 'spec/controllers/registrations_controller_spec.rb' 131 | copy_and_replace 'spec/controllers/sessions_controller_spec.rb' 132 | copy_and_replace 'spec/controllers/users_controller_spec.rb' 133 | 134 | copy_and_replace 'spec/factories/users.rb' 135 | copy_and_replace 'spec/support/api_helpers.rb' 136 | copy_and_replace 'spec/support/user_helpers.rb' 137 | 138 | ## -------------------------------------------------- 139 | ## Remove unwanted files 140 | ## -------------------------------------------------- 141 | 142 | remove_dir('test') 143 | remove_dir('spec/models') 144 | 145 | ## -------------------------------------------------- 146 | ## Set up environment variables 147 | ## -------------------------------------------------- 148 | 149 | create_file '.env' do 150 | "DEVISE_JWT_SECRET_KEY=#{SecureRandom.hex(64)}" 151 | end 152 | -------------------------------------------------------------------------------- /templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | /.env 4 | 5 | # Ignore the default SQLite database. 6 | /db/*.sqlite3 7 | /db/*.sqlite3-journal 8 | /db/*.sqlite3-* 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 | 25 | /public/assets 26 | .byebug_history 27 | 28 | # Ignore master key for decrypting credentials and more. 29 | /config/master.key 30 | 31 | /public/packs 32 | /public/packs-test 33 | /node_modules 34 | /yarn-error.log 35 | yarn-debug.log* 36 | .yarn-integrity 37 | -------------------------------------------------------------------------------- /templates/app/controllers/api/base_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::BaseController < ApplicationController 2 | 3 | before_action :authenticate_user! 4 | rescue_from ActiveRecord::RecordNotFound, with: :not_found 5 | rescue_from ActiveRecord::RecordInvalid, with: :record_invalid 6 | 7 | def not_found 8 | render json: { 9 | 'errors': [ 10 | { 11 | 'status': '404', 12 | 'title': 'Not Found' 13 | } 14 | ] 15 | }, status: 404 16 | end 17 | 18 | def record_invalid(message) 19 | render json: { 20 | 'errors': [ 21 | { 22 | 'status': '400', 23 | 'title': message 24 | } 25 | ] 26 | }, status: 400 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /templates/app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < Api::BaseController 2 | 3 | before_action :find_user, only: %w[show] 4 | 5 | def show 6 | render_jsonapi_response(@user) 7 | end 8 | 9 | private 10 | 11 | def find_user 12 | @user = User.find(params[:id]) 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /templates/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | 3 | rescue_from ActiveRecord::RecordNotUnique, with: :record_not_unique 4 | 5 | def render_jsonapi_response(resource) 6 | if resource.errors.empty? 7 | render jsonapi: resource 8 | else 9 | render jsonapi_errors: resource.errors, status: 400 10 | end 11 | end 12 | 13 | def record_not_unique(message) 14 | render json: { 15 | 'errors': [ 16 | { 17 | 'status': '400', 18 | 'title': message 19 | } 20 | ] 21 | }, status: 400 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /templates/app/controllers/registrations_controller.rb: -------------------------------------------------------------------------------- 1 | class RegistrationsController < Devise::RegistrationsController 2 | 3 | def create 4 | build_resource(sign_up_params) 5 | resource.save 6 | sign_up(resource_name, resource) if resource.persisted? 7 | render_jsonapi_response(resource) 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /templates/app/controllers/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class SessionsController < Devise::SessionsController 2 | 3 | private 4 | 5 | def respond_with(resource, _opts = {}) 6 | render_jsonapi_response(resource) 7 | end 8 | 9 | def respond_to_on_destroy 10 | head :no_content 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /templates/app/models/jwt_denylist.rb: -------------------------------------------------------------------------------- 1 | class JwtDenylist < ApplicationRecord 2 | include Devise::JWT::RevocationStrategies::Denylist 3 | 4 | self.table_name = 'jwt_denylists' 5 | end 6 | -------------------------------------------------------------------------------- /templates/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | devise :database_authenticatable, :registerable, 3 | :jwt_authenticatable, jwt_revocation_strategy: JwtDenylist 4 | end 5 | -------------------------------------------------------------------------------- /templates/app/serializable/SerializableUser.rb: -------------------------------------------------------------------------------- 1 | class SerializableUser < JSONAPI::Serializable::Resource 2 | type 'users' 3 | 4 | attributes :email 5 | 6 | link :self do 7 | @url_helpers.api_user_url(@object.id) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /templates/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Use this hook to configure devise mailer, warden hooks and so forth. 4 | # Many of these configuration options can be set straight in your model. 5 | Devise.setup do |config| 6 | config.jwt do |jwt| 7 | jwt.secret = ENV['DEVISE_JWT_SECRET_KEY'] 8 | jwt.dispatch_requests = [ 9 | ['POST', %r{^/api/login$}] 10 | ] 11 | jwt.revocation_requests = [ 12 | ['DELETE', %r{^/api/logout$}] 13 | ] 14 | jwt.expiration_time = 1.day.to_i 15 | end 16 | # The secret key used by Devise. Devise uses this key to generate 17 | # random tokens. Changing this key will render invalid all existing 18 | # confirmation, reset password and unlock tokens in the database. 19 | # Devise will use the `secret_key_base` as its `secret_key` 20 | # by default. You can change it below and use your own secret key. 21 | # config.secret_key = '19dd50900032a49a4686bf855205e3e3412fe53a326e763aec936089f25054650ab0497b59e42b6e6faeb833ffed049bf981db808fc41ff5fa145f9b76b4135e' 22 | 23 | # ==> Controller configuration 24 | # Configure the parent class to the devise controllers. 25 | # config.parent_controller = 'DeviseController' 26 | 27 | # ==> Mailer Configuration 28 | # Configure the e-mail address which will be shown in Devise::Mailer, 29 | # note that it will be overwritten if you use your own mailer class 30 | # with default "from" parameter. 31 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 32 | 33 | # Configure the class responsible to send e-mails. 34 | # config.mailer = 'Devise::Mailer' 35 | 36 | # Configure the parent class responsible to send e-mails. 37 | # config.parent_mailer = 'ActionMailer::Base' 38 | 39 | # ==> ORM configuration 40 | # Load and configure the ORM. Supports :active_record (default) and 41 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 42 | # available as additional gems. 43 | require 'devise/orm/active_record' 44 | 45 | # ==> Configuration for any authentication mechanism 46 | # Configure which keys are used when authenticating a user. The default is 47 | # just :email. You can configure it to use [:username, :subdomain], so for 48 | # authenticating a user, both parameters are required. Remember that those 49 | # parameters are used only when authenticating and not when retrieving from 50 | # session. If you need permissions, you should implement that in a before filter. 51 | # You can also supply a hash where the value is a boolean determining whether 52 | # or not authentication should be aborted when the value is not present. 53 | # config.authentication_keys = [:email] 54 | 55 | # Configure parameters from the request object used for authentication. Each entry 56 | # given should be a request method and it will automatically be passed to the 57 | # find_for_authentication method and considered in your model lookup. For instance, 58 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 59 | # The same considerations mentioned for authentication_keys also apply to request_keys. 60 | # config.request_keys = [] 61 | 62 | # Configure which authentication keys should be case-insensitive. 63 | # These keys will be downcased upon creating or modifying a user and when used 64 | # to authenticate or find a user. Default is :email. 65 | config.case_insensitive_keys = [:email] 66 | 67 | # Configure which authentication keys should have whitespace stripped. 68 | # These keys will have whitespace before and after removed upon creating or 69 | # modifying a user and when used to authenticate or find a user. Default is :email. 70 | config.strip_whitespace_keys = [:email] 71 | 72 | # Tell if authentication through request.params is enabled. True by default. 73 | # It can be set to an array that will enable params authentication only for the 74 | # given strategies, for example, `config.params_authenticatable = [:database]` will 75 | # enable it only for database (email + password) authentication. 76 | # config.params_authenticatable = true 77 | 78 | # Tell if authentication through HTTP Auth is enabled. False by default. 79 | # It can be set to an array that will enable http authentication only for the 80 | # given strategies, for example, `config.http_authenticatable = [:database]` will 81 | # enable it only for database authentication. The supported strategies are: 82 | # :database = Support basic authentication with authentication key + password 83 | # config.http_authenticatable = false 84 | 85 | # If 401 status code should be returned for AJAX requests. True by default. 86 | # config.http_authenticatable_on_xhr = true 87 | 88 | # The realm used in Http Basic Authentication. 'Application' by default. 89 | # config.http_authentication_realm = 'Application' 90 | 91 | # It will change confirmation, password recovery and other workflows 92 | # to behave the same regardless if the e-mail provided was right or wrong. 93 | # Does not affect registerable. 94 | # config.paranoid = true 95 | 96 | # By default Devise will store the user in session. You can skip storage for 97 | # particular strategies by setting this option. 98 | # Notice that if you are skipping storage for all authentication paths, you 99 | # may want to disable generating routes to Devise's sessions controller by 100 | # passing skip: :sessions to `devise_for` in your config/routes.rb 101 | config.skip_session_storage = [:http_auth] 102 | 103 | # By default, Devise cleans up the CSRF token on authentication to 104 | # avoid CSRF token fixation attacks. This means that, when using AJAX 105 | # requests for sign in and sign up, you need to get a new CSRF token 106 | # from the server. You can disable this option at your own risk. 107 | # config.clean_up_csrf_token_on_authentication = true 108 | 109 | # When false, Devise will not attempt to reload routes on eager load. 110 | # This can reduce the time taken to boot the app but if your application 111 | # requires the Devise mappings to be loaded during boot time the application 112 | # won't boot properly. 113 | # config.reload_routes = true 114 | 115 | # ==> Configuration for :database_authenticatable 116 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 117 | # using other algorithms, it sets how many times you want the password to be hashed. 118 | # 119 | # Limiting the stretches to just one in testing will increase the performance of 120 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 121 | # a value less than 10 in other environments. Note that, for bcrypt (the default 122 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 123 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 124 | config.stretches = Rails.env.test? ? 1 : 11 125 | 126 | # Set up a pepper to generate the hashed password. 127 | # config.pepper = '02a9b26c7e7f8a10a1b1205663dbb954f10c4e72a43f5d05192535f3ae505f4a61f3a5b409d0bd0fa5f5c333b193aaad53c3a7c57471e222a4fb6a7562e779b2' 128 | 129 | # Send a notification to the original email when the user's email is changed. 130 | # config.send_email_changed_notification = false 131 | 132 | # Send a notification email when the user's password is changed. 133 | # config.send_password_change_notification = false 134 | 135 | # ==> Configuration for :confirmable 136 | # A period that the user is allowed to access the website even without 137 | # confirming their account. For instance, if set to 2.days, the user will be 138 | # able to access the website for two days without confirming their account, 139 | # access will be blocked just in the third day. 140 | # You can also set it to nil, which will allow the user to access the website 141 | # without confirming their account. 142 | # Default is 0.days, meaning the user cannot access the website without 143 | # confirming their account. 144 | # config.allow_unconfirmed_access_for = 2.days 145 | 146 | # A period that the user is allowed to confirm their account before their 147 | # token becomes invalid. For example, if set to 3.days, the user can confirm 148 | # their account within 3 days after the mail was sent, but on the fourth day 149 | # their account can't be confirmed with the token any more. 150 | # Default is nil, meaning there is no restriction on how long a user can take 151 | # before confirming their account. 152 | # config.confirm_within = 3.days 153 | 154 | # If true, requires any email changes to be confirmed (exactly the same way as 155 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 156 | # db field (see migrations). Until confirmed, new email is stored in 157 | # unconfirmed_email column, and copied to email column on successful confirmation. 158 | config.reconfirmable = true 159 | 160 | # Defines which key will be used when confirming an account 161 | # config.confirmation_keys = [:email] 162 | 163 | # ==> Configuration for :rememberable 164 | # The time the user will be remembered without asking for credentials again. 165 | # config.remember_for = 2.weeks 166 | 167 | # Invalidates all the remember me tokens when the user signs out. 168 | config.expire_all_remember_me_on_sign_out = true 169 | 170 | # If true, extends the user's remember period when remembered via cookie. 171 | # config.extend_remember_period = false 172 | 173 | # Options to be passed to the created cookie. For instance, you can set 174 | # secure: true in order to force SSL only cookies. 175 | # config.rememberable_options = {} 176 | 177 | # ==> Configuration for :validatable 178 | # Range for password length. 179 | config.password_length = 6..128 180 | 181 | # Email regex used to validate email formats. It simply asserts that 182 | # one (and only one) @ exists in the given string. This is mainly 183 | # to give user feedback and not to assert the e-mail validity. 184 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 185 | 186 | # ==> Configuration for :timeoutable 187 | # The time you want to timeout the user session without activity. After this 188 | # time the user will be asked for credentials again. Default is 30 minutes. 189 | # config.timeout_in = 30.minutes 190 | 191 | # ==> Configuration for :lockable 192 | # Defines which strategy will be used to lock an account. 193 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 194 | # :none = No lock strategy. You should handle locking by yourself. 195 | # config.lock_strategy = :failed_attempts 196 | 197 | # Defines which key will be used when locking and unlocking an account 198 | # config.unlock_keys = [:email] 199 | 200 | # Defines which strategy will be used to unlock an account. 201 | # :email = Sends an unlock link to the user email 202 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 203 | # :both = Enables both strategies 204 | # :none = No unlock strategy. You should handle unlocking by yourself. 205 | # config.unlock_strategy = :both 206 | 207 | # Number of authentication tries before locking an account if lock_strategy 208 | # is failed attempts. 209 | # config.maximum_attempts = 20 210 | 211 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 212 | # config.unlock_in = 1.hour 213 | 214 | # Warn on the last attempt before the account is locked. 215 | # config.last_attempt_warning = true 216 | 217 | # ==> Configuration for :recoverable 218 | # 219 | # Defines which key will be used when recovering the password for an account 220 | # config.reset_password_keys = [:email] 221 | 222 | # Time interval you can reset your password with a reset password key. 223 | # Don't put a too small interval or your users won't have the time to 224 | # change their passwords. 225 | config.reset_password_within = 6.hours 226 | 227 | # When set to false, does not sign a user in automatically after their password is 228 | # reset. Defaults to true, so a user is signed in automatically after a reset. 229 | # config.sign_in_after_reset_password = true 230 | 231 | # ==> Configuration for :encryptable 232 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 233 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 234 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 235 | # for default behavior) and :restful_authentication_sha1 (then you should set 236 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 237 | # 238 | # Require the `devise-encryptable` gem when using anything other than bcrypt 239 | # config.encryptor = :sha512 240 | 241 | # ==> Scopes configuration 242 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 243 | # "users/sessions/new". It's turned off by default because it's slower if you 244 | # are using only default views. 245 | # config.scoped_views = false 246 | 247 | # Configure the default scope given to Warden. By default it's the first 248 | # devise role declared in your routes (usually :user). 249 | # config.default_scope = :user 250 | 251 | # Set this configuration to false if you want /users/sign_out to sign out 252 | # only the current scope. By default, Devise signs out all scopes. 253 | # config.sign_out_all_scopes = true 254 | config.navigational_formats = [] 255 | # ==> Navigation configuration 256 | # Lists the formats that should be treated as navigational. Formats like 257 | # :html, should redirect to the sign in page when the user does not have 258 | # access, but formats like :xml or :json, should return 401. 259 | # 260 | # If you have any extra navigational formats, like :iphone or :mobile, you 261 | # should add them to the navigational formats lists. 262 | # 263 | # The "*/*" below is required to match Internet Explorer requests. 264 | # config.navigational_formats = ['*/*', :html] 265 | 266 | # The default HTTP method used to sign out a resource. Default is :delete. 267 | config.sign_out_via = :delete 268 | 269 | # ==> OmniAuth 270 | # Add a new OmniAuth provider. Check the wiki for more information on setting 271 | # up on your models and hooks. 272 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 273 | 274 | # ==> Warden configuration 275 | # If you want to use other strategies, that are not supported by Devise, or 276 | # change the failure app, you can configure them inside the config.warden block. 277 | # 278 | # config.warden do |manager| 279 | # manager.intercept_401 = false 280 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 281 | # end 282 | 283 | # ==> Mountable engine configurations 284 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 285 | # is mountable, there are some extra configurations to be taken into account. 286 | # The following options are available, assuming the engine is mounted as: 287 | # 288 | # mount MyEngine, at: '/my_engine' 289 | # 290 | # The router that invoked `devise_for`, in the example above, would be: 291 | # config.router_name = :my_engine 292 | # 293 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 294 | # so you need to do it manually. For the users scope, it would be: 295 | # config.omniauth_path_prefix = '/my_engine/users/auth' 296 | 297 | # ==> Turbolinks configuration 298 | # If your app is using Turbolinks, Turbolinks::Controller needs to be included to make redirection work correctly: 299 | # 300 | # ActiveSupport.on_load(:devise_failure_app) do 301 | # include Turbolinks::Controller 302 | # end 303 | 304 | # ==> Configuration for :registerable 305 | 306 | # When set to false, does not sign a user in automatically after their password is 307 | # changed. Defaults to true, so a user is signed in automatically after changing a password. 308 | # config.sign_in_after_change_password = true 309 | end 310 | -------------------------------------------------------------------------------- /templates/config/initializers/rack_cors.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 2 | allow do 3 | origins '*' 4 | resource '*', 5 | headers: %w(Authorization), 6 | expose: %w(Authorization), 7 | methods: :any 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /templates/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.default_url_options[:host] = 'localhost:3001' 2 | 3 | Rails.application.routes.draw do 4 | 5 | namespace :api, defaults: { format: :json } do 6 | resources :users, only: %w[show] 7 | end 8 | 9 | devise_for :users, 10 | defaults: { format: :json }, 11 | path: '', 12 | path_names: { 13 | sign_in: 'api/login', 14 | sign_out: 'api/logout', 15 | registration: 'api/signup' 16 | }, 17 | controllers: { 18 | sessions: 'sessions', 19 | registrations: 'registrations' 20 | } 21 | 22 | end 23 | -------------------------------------------------------------------------------- /templates/spec/controllers/registrations_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe RegistrationsController, type: :request do 4 | 5 | let (:user) { build_user } 6 | let (:existing_user) { create_user } 7 | let (:signup_url) { '/api/signup' } 8 | 9 | context 'When creating a new user' do 10 | before do 11 | post signup_url, params: { 12 | user: { 13 | email: user.email, 14 | password: user.password 15 | } 16 | } 17 | end 18 | 19 | it 'returns 200' do 20 | expect(response.status).to eq(200) 21 | end 22 | 23 | it 'returns a token' do 24 | expect(response.headers['Authorization']).to be_present 25 | end 26 | 27 | it 'returns the user email' do 28 | expect(json['data']).to have_attribute(:email).with_value(user.email) 29 | end 30 | end 31 | 32 | context 'When an email already exists' do 33 | before do 34 | post signup_url, params: { 35 | user: { 36 | email: existing_user.email, 37 | password: existing_user.password 38 | } 39 | } 40 | end 41 | 42 | it 'returns 400' do 43 | expect(response.status).to eq(400) 44 | end 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /templates/spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe SessionsController, type: :request do 4 | 5 | let (:user) { create_user } 6 | let (:login_url) { '/api/login' } 7 | let (:logout_url) { '/api/logout' } 8 | 9 | context 'When logging in' do 10 | before do 11 | login_with_api(user) 12 | end 13 | 14 | it 'returns a token' do 15 | expect(response.headers['Authorization']).to be_present 16 | end 17 | 18 | it 'returns 200' do 19 | expect(response.status).to eq(200) 20 | end 21 | end 22 | 23 | context 'When password is missing' do 24 | before do 25 | post login_url, params: { 26 | user: { 27 | email: user.email, 28 | password: nil 29 | } 30 | } 31 | end 32 | 33 | it 'returns 401' do 34 | expect(response.status).to eq(401) 35 | end 36 | 37 | end 38 | 39 | context 'When logging out' do 40 | it 'returns 204' do 41 | delete logout_url 42 | 43 | expect(response).to have_http_status(204) 44 | end 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /templates/spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | describe Api::UsersController, type: :request do 4 | 5 | let (:user) { create_user } 6 | 7 | context 'When fetching a user' do 8 | before do 9 | login_with_api(user) 10 | get "/api/users/#{user.id}", headers: { 11 | 'Authorization': response.headers['Authorization'] 12 | } 13 | end 14 | 15 | it 'returns 200' do 16 | expect(response.status).to eq(200) 17 | end 18 | 19 | it 'returns the user' do 20 | expect(json['data']).to have_id(user.id.to_s) 21 | expect(json['data']).to have_type('users') 22 | end 23 | end 24 | 25 | context 'When a user is missing' do 26 | before do 27 | login_with_api(user) 28 | get "/api/users/blank", headers: { 29 | 'Authorization': response.headers['Authorization'] 30 | } 31 | end 32 | 33 | it 'returns 404' do 34 | expect(response.status).to eq(404) 35 | end 36 | end 37 | 38 | context 'When the Authorization header is missing' do 39 | before do 40 | get "/api/users/#{user.id}" 41 | end 42 | 43 | it 'returns 401' do 44 | expect(response.status).to eq(401) 45 | end 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /templates/spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /templates/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | 2 | # This file is copied to spec/ when you run 'rails generate rspec:install' 3 | require 'spec_helper' 4 | require 'jsonapi/rspec' 5 | require 'vcr' 6 | ENV['RAILS_ENV'] ||= 'test' 7 | require File.expand_path('../../config/environment', __FILE__) 8 | # Prevent database truncation if the environment is production 9 | abort("The Rails environment is running in production mode!") if Rails.env.production? 10 | require 'rspec/rails' 11 | # Add additional requires below this line. Rails is not loaded until this point! 12 | 13 | # Requires supporting ruby files with custom matchers and macros, etc, in 14 | Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 15 | # run as spec files by default. This means that files in spec/support that end 16 | # in _spec.rb will both be required and run as specs, causing the specs to be 17 | # run twice. It is recommended that you do not name files matching this glob to 18 | # end with _spec.rb. You can configure this pattern with the --pattern 19 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 20 | # 21 | # The following line is provided for convenience purposes. It has the downside 22 | # of increasing the boot-up time by auto-requiring all files in the support 23 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 24 | # require only the support files necessary. 25 | # 26 | # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } 27 | 28 | # Checks for pending migrations and applies them before tests are run. 29 | # If you are not using ActiveRecord, you can remove these lines. 30 | begin 31 | ActiveRecord::Migration.maintain_test_schema! 32 | rescue ActiveRecord::PendingMigrationError => e 33 | puts e.to_s.strip 34 | exit 1 35 | end 36 | 37 | VCR.configure do |c| 38 | c.cassette_library_dir = "#{Rails.root}/spec/vcr_cassettes" 39 | c.hook_into :webmock 40 | c.ignore_localhost = true 41 | c.allow_http_connections_when_no_cassette = true 42 | c.configure_rspec_metadata! 43 | end 44 | 45 | Shoulda::Matchers.configure do |config| 46 | config.integrate do |with| 47 | with.test_framework :rspec 48 | with.library :rails 49 | end 50 | end 51 | 52 | RSpec::Matchers.define :be_url do |expected| 53 | match do |actual| 54 | actual =~ URI::DEFAULT_PARSER.make_regexp 55 | end 56 | end 57 | 58 | RSpec.configure do |config| 59 | config.tty = true 60 | config.formatter = :documentation 61 | 62 | config.include JSONAPI::RSpec 63 | # Support for documents with mixed string/symbol keys. Disabled by default. 64 | # config.jsonapi_indifferent_hash = true 65 | 66 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 67 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 68 | config.include ApiHelpers 69 | config.include UserHelpers 70 | config.include Devise::Test::ControllerHelpers, type: :controller 71 | 72 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 73 | # examples within a transaction, remove the following line or assign false 74 | # instead of true. 75 | config.use_transactional_fixtures = true 76 | 77 | # RSpec Rails can automatically mix in different behaviours to your tests 78 | # based on their file location, for example enabling you to call `get` and 79 | # `post` in specs under `spec/controllers`. 80 | # 81 | # You can disable this behaviour by removing the line below, and instead 82 | # explicitly tag your specs with their type, e.g.: 83 | # 84 | # RSpec.describe UsersController, :type => :controller do 85 | # # ... 86 | # end 87 | # 88 | # The different available types are documented in the features, such as in 89 | # https://relishapp.com/rspec/rspec-rails/docs 90 | config.infer_spec_type_from_file_location! 91 | 92 | # Filter lines from Rails gems in backtraces. 93 | config.filter_rails_from_backtrace! 94 | # arbitrary gems may also be filtered via: 95 | # config.filter_gems_from_backtrace("gem name") 96 | end 97 | -------------------------------------------------------------------------------- /templates/spec/support/api_helpers.rb: -------------------------------------------------------------------------------- 1 | module ApiHelpers 2 | 3 | def json 4 | JSON.parse(response.body) 5 | end 6 | 7 | def login_with_api(user) 8 | post '/api/login', params: { 9 | user: { 10 | email: user.email, 11 | password: user.password 12 | } 13 | } 14 | end 15 | 16 | def set_devise_mapping 17 | request.env['devise.mapping'] = Devise.mappings[:user] 18 | end 19 | 20 | end 21 | -------------------------------------------------------------------------------- /templates/spec/support/user_helpers.rb: -------------------------------------------------------------------------------- 1 | require 'faker' 2 | module UserHelpers 3 | 4 | def build_user 5 | FactoryBot.build(:user, email: Faker::Internet.email, password: Faker::Internet.password) 6 | end 7 | 8 | def create_user 9 | FactoryBot.create(:user, email: Faker::Internet.email, password: Faker::Internet.password) 10 | end 11 | 12 | end 13 | --------------------------------------------------------------------------------