├── log └── .keep ├── tmp └── .keep ├── lib └── tasks │ └── .keep ├── app ├── models │ ├── concerns │ │ └── .keep │ ├── application_record.rb │ └── user.rb ├── controllers │ ├── concerns │ │ └── .keep │ ├── home_controller.rb │ ├── application_controller.rb │ └── api │ │ └── v1 │ │ ├── api_controller.rb │ │ └── users_controller.rb ├── views │ └── layouts │ │ ├── mailer.text.erb │ │ └── mailer.html.erb ├── serializers │ └── user_serializer.rb ├── channels │ └── application_cable │ │ ├── channel.rb │ │ └── connection.rb ├── workers │ └── sample_worker.rb └── mailers │ └── application_mailer.rb ├── .rspec ├── config ├── initializers │ ├── active_model_serializer.rb │ ├── mime_types.rb │ ├── application_controller_renderer.rb │ ├── filter_parameter_logging.rb │ ├── devise.rb │ ├── backtrace_silencers.rb │ ├── wrap_parameters.rb │ ├── cors.rb │ ├── inflections.rb │ ├── new_framework_defaults.rb │ ├── rack_attack.rb │ └── devise_token_auth.rb ├── spring.rb ├── boot.rb ├── environment.rb ├── cable.yml ├── routes.rb ├── locales │ └── en.yml ├── secrets.yml ├── application.rb ├── environments │ ├── development.rb │ ├── test.rb │ └── production.rb ├── puma.rb └── database.yml ├── bin ├── bundle ├── rake ├── rails ├── spring ├── update └── setup ├── config.ru ├── spec ├── factories.rb ├── controllers │ └── api │ │ └── v1 │ │ └── users_controller_spec.rb ├── rails_helper.rb └── spec_helper.rb ├── public └── robots.txt ├── Rakefile ├── db ├── seeds.rb ├── migrate │ └── 20160723214324_devise_token_auth_create_users.rb └── schema.rb ├── .gitignore ├── Gemfile ├── Gemfile.lock └── README.md /log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require rails_helper 3 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /config/initializers/active_model_serializer.rb: -------------------------------------------------------------------------------- 1 | ActiveModelSerializers.config.adapter = :json_api 2 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :email 3 | end 4 | -------------------------------------------------------------------------------- /app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def show 3 | render json: {} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/workers/sample_worker.rb: -------------------------------------------------------------------------------- 1 | class SampleWorker 2 | include Sidekiq::Worker 3 | def perform() 4 | # do something 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'from@example.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include DeviseTokenAuth::Concerns::SetUserByToken 3 | end 4 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | factory :user do 3 | name "John Doe" 4 | email "test@test.com" 5 | password "testpass1234" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /app/controllers/api/v1/api_controller.rb: -------------------------------------------------------------------------------- 1 | module Api::V1 2 | class ApiController < ApplicationController 3 | before_action :authenticate_user! 4 | respond_to :json 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | Devise.setup do |config| 2 | # Using rails-api, tell devise to not use ActionDispatch::Flash 3 | # middleware b/c rails-api does not include it. 4 | config.navigational_formats = [:json] 5 | config.mailer_sender = "support@myapp.com" 6 | end 7 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | root to: 'home#show' 4 | 5 | mount_devise_token_auth_for 'User', at: 'auth' 6 | 7 | scope module: 'api' do 8 | namespace :v1 do 9 | resources :users, only: [:index, :show] 10 | end 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | # Include default devise modules. 3 | devise :database_authenticatable, :registerable, 4 | :recoverable, :rememberable, :trackable, :validatable, 5 | :confirmable, :omniauthable 6 | include DeviseTokenAuth::Concerns::User 7 | end 8 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | module Api::V1 2 | class UsersController < ApiController 3 | 4 | # GET /v1/users 5 | def index 6 | render json: User.all 7 | end 8 | 9 | # GET /v1/users/{id} 10 | def show 11 | render json: User.find(params[:id]) 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /.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 Byebug command history file. 17 | .byebug_history 18 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | if (match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)) 11 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/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 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /config/initializers/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 8 | # Previous versions had false. 9 | ActiveSupport.to_time_preserves_timezone = true 10 | 11 | # Require `belongs_to` associations by default. Previous versions had false. 12 | Rails.application.config.active_record.belongs_to_required_by_default = true 13 | 14 | # Do not halt callback chains when a callback returns false. Previous versions had true. 15 | ActiveSupport.halt_callback_chains_on_return_false = false 16 | 17 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 18 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 19 | -------------------------------------------------------------------------------- /config/initializers/rack_attack.rb: -------------------------------------------------------------------------------- 1 | class Rack::Attack 2 | 3 | # `Rack::Attack` is configured to use the `Rails.cache` value by default, 4 | # but you can override that by setting the `Rack::Attack.cache.store` value 5 | # Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new 6 | 7 | # Allow all local traffic 8 | safelist('allow-localhost') do |req| 9 | '127.0.0.1' == req.ip || '::1' == req.ip 10 | end 11 | 12 | # Allow an IP address to make 5 requests every 5 seconds 13 | throttle('req/ip', limit: 5, period: 5) do |req| 14 | req.ip 15 | end 16 | 17 | # Send the following response to throttled clients 18 | self.throttled_response = ->(env) { 19 | retry_after = (env['rack.attack.match_data'] || {})[:period] 20 | [ 21 | 429, 22 | {'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s}, 23 | [{error: "Throttle limit reached. Retry later."}.to_json] 24 | ] 25 | } 26 | end 27 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rails secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 3e23d2ce0ca8f32de8ee7ddd6f3be3994e5cd958a2ac467a767ebd79ce4bdade9913e880aee5ccc611159bdf0ba5bc5c8a7855e0c032f8219ab5d77150f8e228 15 | 16 | test: 17 | secret_key_base: 7e0eadc7b11f7b84c6ebbba796698bd09b5ba8c1ef2b303153852625970440dc8148174472399912f0489a95cbf448ba7a105fc174f7e4469a1aada1e8c08a5e 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /spec/controllers/api/v1/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Api::V1::UsersController do 4 | 5 | describe 'GET #show' do 6 | 7 | before(:each) do 8 | @user = FactoryBot.create :user 9 | auth_headers = @user.create_new_auth_token 10 | request.headers.merge!(auth_headers) 11 | get :show, id: @user.id 12 | end 13 | 14 | it 'responds with 200 status code' do 15 | expect(response.code).to eq('200') 16 | end 17 | 18 | it 'returns the serialized user attributes' do 19 | expect(JSON.parse(response.body)['data']['attributes']).to eq({'name'=>'John Doe', 'email'=>'test@test.com'}) 20 | end 21 | 22 | end 23 | 24 | describe 'GET #index' do 25 | 26 | before(:each) do 27 | @user = FactoryBot.create :user 28 | auth_headers = @user.create_new_auth_token 29 | request.headers.merge!(auth_headers) 30 | get :index, id: @user.id 31 | end 32 | 33 | it 'responds with 200 status code' do 34 | expect(response.code).to eq('200') 35 | end 36 | 37 | it 'returns the serialized user attributes' do 38 | expect(JSON.parse(response.body)['data'].length).to eq(1) 39 | expect(JSON.parse(response.body)['data'].first['attributes']).to eq({'name'=>'John Doe', 'email'=>'test@test.com'}) 40 | end 41 | 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' 5 | gem 'rails', '~> 5.0.0' 6 | # Use postgresql as the database for Active Record 7 | gem 'pg' 8 | # Use Puma as the app server 9 | gem 'puma' 10 | # use Devise Token Auth for token based authentication 11 | gem 'devise_token_auth' 12 | gem 'omniauth' 13 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible 14 | gem 'rack-cors' 15 | # Use Rack attack for black/white listing, rate limiting, throttling 16 | gem 'rack-attack' 17 | # Actime Model Serializers provides a good way to serialize AR objects 18 | gem 'active_model_serializers' 19 | # redis-rails for redis cacheing 20 | gem 'redis-rails' 21 | # Use sidekiq for background job processing 22 | gem 'sidekiq' 23 | 24 | # Use Redis adapter to run Action Cable in production 25 | # gem 'redis', '~> 3.0' 26 | 27 | group :development, :test do 28 | gem 'rspec-rails' 29 | gem 'factory_bot_rails' 30 | gem 'pry-rails' 31 | gem 'pry-byebug' 32 | gem 'pry-stack_explorer' 33 | end 34 | 35 | group :development do 36 | gem 'listen' 37 | # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring 38 | gem 'spring' 39 | gem 'spring-watcher-listen' 40 | end 41 | 42 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 43 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 44 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | # require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module GenericApi 20 | class Application < Rails::Application 21 | # Settings in config/environments/* take precedence over those specified here. 22 | # Application configuration should go into files in config/initializers 23 | # -- all .rb files in that directory are automatically loaded. 24 | 25 | # Only loads a smaller set of middleware suitable for API only apps. 26 | # Middleware like session, flash, cookies can be added back manually. 27 | # Skip views, helpers and assets when generating a new resource. 28 | config.api_only = true 29 | 30 | # The following will allow cross domain requests from any domain. 31 | # FIXME: Make sure to whitelist only the needed domains. 32 | config.middleware.use Rack::Cors do 33 | allow do 34 | origins '*' 35 | resource '*', 36 | :headers => :any, 37 | :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'], 38 | :methods => [:get, :post, :options, :delete, :put] 39 | end 40 | end 41 | 42 | config.middleware.use Rack::Attack 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /db/migrate/20160723214324_devise_token_auth_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseTokenAuthCreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table(:users) do |t| 4 | ## Required 5 | t.string :provider, :null => false, :default => "email" 6 | t.string :uid, :null => false, :default => "" 7 | 8 | ## Database authenticatable 9 | t.string :encrypted_password, :null => false, :default => "" 10 | 11 | ## Recoverable 12 | t.string :reset_password_token 13 | t.datetime :reset_password_sent_at 14 | 15 | ## Rememberable 16 | t.datetime :remember_created_at 17 | 18 | ## Trackable 19 | t.integer :sign_in_count, :default => 0, :null => false 20 | t.datetime :current_sign_in_at 21 | t.datetime :last_sign_in_at 22 | t.string :current_sign_in_ip 23 | t.string :last_sign_in_ip 24 | 25 | ## Confirmable 26 | t.string :confirmation_token 27 | t.datetime :confirmed_at 28 | t.datetime :confirmation_sent_at 29 | t.string :unconfirmed_email # Only if using reconfirmable 30 | 31 | ## Lockable 32 | # t.integer :failed_attempts, :default => 0, :null => false # Only if lock strategy is :failed_attempts 33 | # t.string :unlock_token # Only if unlock strategy is :email or :both 34 | # t.datetime :locked_at 35 | 36 | ## User Info 37 | t.string :name 38 | t.string :nickname 39 | t.string :image 40 | t.string :email 41 | 42 | ## Tokens 43 | t.json :tokens 44 | 45 | t.timestamps 46 | end 47 | 48 | add_index :users, :email 49 | add_index :users, [:uid, :provider], :unique => true 50 | add_index :users, :reset_password_token, :unique => true 51 | # add_index :users, :confirmation_token, :unique => true 52 | # add_index :users, :unlock_token, :unique => true 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if ENV['REDIS_URL'] 17 | config.action_controller.perform_caching = true 18 | config.cache_store = :redis_store, ENV['REDIS_URL'] 19 | else 20 | config.action_controller.perform_caching = false 21 | config.cache_store = :null_store 22 | end 23 | 24 | # Don't care if the mailer can't send. 25 | config.action_mailer.raise_delivery_errors = false 26 | 27 | config.action_mailer.perform_caching = false 28 | 29 | config.action_mailer.delivery_method = :smtp 30 | config.action_mailer.smtp_settings = { address: 'localhost', port: 1025 } 31 | config.action_mailer.default_url_options = { :host => 'localhost', port: '3000' } 32 | 33 | # Print deprecation notices to the Rails logger. 34 | config.active_support.deprecation = :log 35 | 36 | # Raise an error on page load if there are pending migrations. 37 | config.active_record.migration_error = :page_load 38 | 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | 43 | # Use an evented file watcher to asynchronously detect changes in source code, 44 | # routes, locales, etc. This feature depends on the listen gem. 45 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 46 | end 47 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /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 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20160723214324) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "users", force: :cascade do |t| 19 | t.string "provider", default: "email", null: false 20 | t.string "uid", default: "", null: false 21 | t.string "encrypted_password", default: "", null: false 22 | t.string "reset_password_token" 23 | t.datetime "reset_password_sent_at" 24 | t.datetime "remember_created_at" 25 | t.integer "sign_in_count", default: 0, null: false 26 | t.datetime "current_sign_in_at" 27 | t.datetime "last_sign_in_at" 28 | t.string "current_sign_in_ip" 29 | t.string "last_sign_in_ip" 30 | t.string "confirmation_token" 31 | t.datetime "confirmed_at" 32 | t.datetime "confirmation_sent_at" 33 | t.string "unconfirmed_email" 34 | t.string "name" 35 | t.string "nickname" 36 | t.string "image" 37 | t.string "email" 38 | t.json "tokens" 39 | t.datetime "created_at" 40 | t.datetime "updated_at" 41 | t.index ["email"], name: "index_users_on_email", using: :btree 42 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree 43 | t.index ["uid", "provider"], name: "index_users_on_uid_and_provider", unique: true, using: :btree 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /config/initializers/devise_token_auth.rb: -------------------------------------------------------------------------------- 1 | DeviseTokenAuth.setup do |config| 2 | # By default the authorization headers will change after each request. The 3 | # client is responsible for keeping track of the changing tokens. Change 4 | # this to false to prevent the Authorization header from changing after 5 | # each request. 6 | # config.change_headers_on_each_request = true 7 | 8 | # By default, users will need to re-authenticate after 2 weeks. This setting 9 | # determines how long tokens will remain valid after they are issued. 10 | # config.token_lifespan = 2.weeks 11 | 12 | # Sets the max number of concurrent devices per user, which is 10 by default. 13 | # After this limit is reached, the oldest tokens will be removed. 14 | # config.max_number_of_devices = 10 15 | 16 | # Sometimes it's necessary to make several requests to the API at the same 17 | # time. In this case, each request in the batch will need to share the same 18 | # auth token. This setting determines how far apart the requests can be while 19 | # still using the same auth token. 20 | # config.batch_request_buffer_throttle = 5.seconds 21 | 22 | # This route will be the prefix for all oauth2 redirect callbacks. For 23 | # example, using the default '/omniauth', the github oauth2 provider will 24 | # redirect successful authentications to '/omniauth/github/callback' 25 | # config.omniauth_prefix = "/omniauth" 26 | 27 | # By default sending current password is not needed for the password update. 28 | # Uncomment to enforce current_password param to be checked before all 29 | # attribute updates. Set it to :password if you want it to be checked only if 30 | # password is updated. 31 | # config.check_current_password_before_update = :attributes 32 | 33 | # By default we will use callbacks for single omniauth. 34 | # It depends on fields like email, provider and uid. 35 | # config.default_callbacks = true 36 | 37 | # Makes it possible to change the headers names 38 | # config.headers_names = {:'access-token' => 'access-token', 39 | # :'client' => 'client', 40 | # :'expiry' => 'expiry', 41 | # :'uid' => 'uid', 42 | # :'token-type' => 'token-type' } 43 | 44 | # By default, only Bearer Token authentication is implemented out of the box. 45 | # If, however, you wish to integrate with legacy Devise authentication, you can 46 | # do so by enabling this flag. NOTE: This feature is highly experimental! 47 | # config.enable_standard_devise_support = false 48 | end 49 | -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV["RAILS_ENV"] ||= 'test' 3 | require 'spec_helper' 4 | require File.expand_path("../../config/environment", __FILE__) 5 | require 'rspec/rails' 6 | # Add additional requires below this line. Rails is not loaded until this point! 7 | 8 | # Requires supporting ruby files with custom matchers and macros, etc, in 9 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 10 | # run as spec files by default. This means that files in spec/support that end 11 | # in _spec.rb will both be required and run as specs, causing the specs to be 12 | # run twice. It is recommended that you do not name files matching this glob to 13 | # end with _spec.rb. You can configure this pattern with the --pattern 14 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 15 | # 16 | # The following line is provided for convenience purposes. It has the downside 17 | # of increasing the boot-up time by auto-requiring all files in the support 18 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 19 | # require only the support files necessary. 20 | # 21 | # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } 22 | 23 | # Checks for pending migrations before tests are run. 24 | # If you are not using ActiveRecord, you can remove this line. 25 | ActiveRecord::Migration.maintain_test_schema! 26 | 27 | RSpec.configure do |config| 28 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 29 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 30 | 31 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 32 | # examples within a transaction, remove the following line or assign false 33 | # instead of true. 34 | config.use_transactional_fixtures = true 35 | 36 | # RSpec Rails can automatically mix in different behaviours to your tests 37 | # based on their file location, for example enabling you to call `get` and 38 | # `post` in specs under `spec/controllers`. 39 | # 40 | # You can disable this behaviour by removing the line below, and instead 41 | # explicitly tag your specs with their type, e.g.: 42 | # 43 | # RSpec.describe UsersController, :type => :controller do 44 | # # ... 45 | # end 46 | # 47 | # The different available types are documented in the features, such as in 48 | # https://relishapp.com/rspec/rspec-rails/docs 49 | config.infer_spec_type_from_file_location! 50 | 51 | config.include Devise::Test::ControllerHelpers, type: :controller 52 | end 53 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # PostgreSQL. Versions 9.1 and up are supported. 2 | # 3 | # Install the pg driver: 4 | # gem install pg 5 | # On OS X with Homebrew: 6 | # gem install pg -- --with-pg-config=/usr/local/bin/pg_config 7 | # On OS X 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 | # http://guides.rubyonrails.org/configuring.html#database-pooling 22 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 23 | 24 | development: 25 | <<: *default 26 | database: generic_api_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 that initialized the database. 32 | 33 | # The password associated with the postgres role (username). 34 | #password: 35 | 36 | # Connect on a TCP socket. Omitted by default since the client uses a 37 | # domain socket that doesn't need configuration. Windows does not have 38 | # domain sockets, so uncomment these lines. 39 | #host: localhost 40 | 41 | # The TCP port the server listens on. Defaults to 5432. 42 | # If your server runs on a different port number, change accordingly. 43 | #port: 5432 44 | 45 | # Schema search path. The server defaults to $user,public 46 | #schema_search_path: myapp,sharedapp,public 47 | 48 | # Minimum log levels, in increasing order: 49 | # debug5, debug4, debug3, debug2, debug1, 50 | # log, notice, warning, error, fatal, and panic 51 | # Defaults to warning. 52 | #min_messages: notice 53 | 54 | # Warning: The database defined as "test" will be erased and 55 | # re-generated from your development database when you run "rake". 56 | # Do not set this db to the same as development or production. 57 | test: 58 | <<: *default 59 | database: generic_api_test 60 | 61 | # As with config/secrets.yml, you never want to store sensitive information, 62 | # like your database password, in your source code. If your source code is 63 | # ever seen by anyone, they now have access to your database. 64 | # 65 | # Instead, provide the password as a unix environment variable when you boot 66 | # the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database 67 | # for a full rundown on how to provide these environment variables in a 68 | # production deployment. 69 | # 70 | # On Heroku and other platform providers, you may have a full connection URL 71 | # available as an environment variable. For example: 72 | # 73 | # DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" 74 | # 75 | # You can use this database configuration with: 76 | # 77 | # production: 78 | # url: <%= ENV['DATABASE_URL'] %> 79 | # 80 | production: 81 | <<: *default 82 | database: generic_api_production 83 | username: devboard 84 | password: <%= ENV['DEVBOARD_DATABASE_PASSWORD'] %> 85 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | 22 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 23 | # config.action_controller.asset_host = 'http://assets.example.com' 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 28 | 29 | # Mount Action Cable outside main process or domain 30 | # config.action_cable.mount_path = nil 31 | # config.action_cable.url = 'wss://example.com/cable' 32 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | # config.force_ssl = true 36 | 37 | # Use the lowest log level to ensure availability of diagnostic information 38 | # when problems arise. 39 | config.log_level = :debug 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | config.cache_store = :redis_store, ENV['REDIS_URL'] 46 | 47 | # Use a real queuing backend for Active Job (and separate queues per environment) 48 | config.action_mailer.perform_caching = false 49 | 50 | # Ignore bad email addresses and do not raise email delivery errors. 51 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 55 | # the I18n.default_locale when a translation cannot be found). 56 | config.i18n.fallbacks = true 57 | 58 | # Send deprecation notices to registered listeners. 59 | config.active_support.deprecation = :notify 60 | 61 | # Use default logging formatter so that PID and timestamp are not suppressed. 62 | config.log_formatter = ::Logger::Formatter.new 63 | 64 | # Use a different logger for distributed setups. 65 | # require 'syslog/logger' 66 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 67 | 68 | if ENV["RAILS_LOG_TO_STDOUT"].present? 69 | logger = ActiveSupport::Logger.new(STDOUT) 70 | logger.formatter = config.log_formatter 71 | config.logger = ActiveSupport::TaggedLogging.new(logger) 72 | end 73 | 74 | # Do not dump schema after migrations. 75 | config.active_record.dump_schema_after_migration = false 76 | end 77 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'factory_bot_rails' 2 | 3 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 4 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 5 | # The generated `.rspec` file contains `--require spec_helper` which will cause this 6 | # file to always be loaded, without a need to explicitly require it in any files. 7 | # 8 | # Given that it is always loaded, you are encouraged to keep this file as 9 | # light-weight as possible. Requiring heavyweight dependencies from this file 10 | # will add to the boot time of your test suite on EVERY test run, even for an 11 | # individual file that may not need all of that loaded. Instead, consider making 12 | # a separate helper file that requires the additional dependencies and performs 13 | # the additional setup, and require it from the spec files that actually need it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | config.include FactoryBot::Syntax::Methods 44 | # The settings below are suggested to provide a good initial experience 45 | # with RSpec, but feel free to customize to your heart's content. 46 | =begin 47 | # These two settings work together to allow you to limit a spec run 48 | # to individual examples or groups you care about by tagging them with 49 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 50 | # get run. 51 | config.filter_run :focus 52 | config.run_all_when_everything_filtered = true 53 | 54 | # Limits the available syntax to the non-monkey patched syntax that is recommended. 55 | # For more details, see: 56 | # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax 57 | # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 58 | # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching 59 | config.disable_monkey_patching! 60 | 61 | # Many RSpec users commonly either run the entire suite or an individual 62 | # file, and it's useful to allow more verbose output when running an 63 | # individual spec file. 64 | if config.files_to_run.one? 65 | # Use the documentation formatter for detailed output, 66 | # unless a formatter has already been configured 67 | # (e.g. via a command-line flag). 68 | config.default_formatter = 'doc' 69 | end 70 | 71 | # Print the 10 slowest examples and example groups at the 72 | # end of the spec run, to help surface which specs are running 73 | # particularly slow. 74 | config.profile_examples = 10 75 | 76 | # Run specs in random order to surface order dependencies. If you find an 77 | # order dependency and want to debug it, you can fix the order by providing 78 | # the seed, which is printed after each run. 79 | # --seed 1234 80 | config.order = :random 81 | 82 | # Seed global randomization in this process using the `--seed` CLI option. 83 | # Setting this allows you to use `--seed` to deterministically reproduce 84 | # test failures related to randomization by passing the same `--seed` value 85 | # as the one that triggered the failure. 86 | Kernel.srand config.seed 87 | =end 88 | end 89 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.0.6) 5 | actionpack (= 5.0.6) 6 | nio4r (>= 1.2, < 3.0) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.0.6) 9 | actionpack (= 5.0.6) 10 | actionview (= 5.0.6) 11 | activejob (= 5.0.6) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.0.6) 15 | actionview (= 5.0.6) 16 | activesupport (= 5.0.6) 17 | rack (~> 2.0) 18 | rack-test (~> 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.0.6) 22 | activesupport (= 5.0.6) 23 | builder (~> 3.1) 24 | erubis (~> 2.7.0) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.3) 27 | active_model_serializers (0.10.7) 28 | actionpack (>= 4.1, < 6) 29 | activemodel (>= 4.1, < 6) 30 | case_transform (>= 0.2) 31 | jsonapi-renderer (>= 0.1.1.beta1, < 0.3) 32 | activejob (5.0.6) 33 | activesupport (= 5.0.6) 34 | globalid (>= 0.3.6) 35 | activemodel (5.0.6) 36 | activesupport (= 5.0.6) 37 | activerecord (5.0.6) 38 | activemodel (= 5.0.6) 39 | activesupport (= 5.0.6) 40 | arel (~> 7.0) 41 | activesupport (5.0.6) 42 | concurrent-ruby (~> 1.0, >= 1.0.2) 43 | i18n (~> 0.7) 44 | minitest (~> 5.1) 45 | tzinfo (~> 1.1) 46 | arel (7.1.4) 47 | bcrypt (3.1.11) 48 | binding_of_caller (0.7.3) 49 | debug_inspector (>= 0.0.1) 50 | builder (3.2.3) 51 | byebug (9.1.0) 52 | case_transform (0.2) 53 | activesupport 54 | coderay (1.1.2) 55 | concurrent-ruby (1.0.5) 56 | connection_pool (2.2.1) 57 | crass (1.0.3) 58 | debug_inspector (0.0.3) 59 | devise (4.3.0) 60 | bcrypt (~> 3.0) 61 | orm_adapter (~> 0.1) 62 | railties (>= 4.1.0, < 5.2) 63 | responders 64 | warden (~> 1.2.3) 65 | devise_token_auth (0.1.42) 66 | devise (> 3.5.2, <= 4.3) 67 | rails (< 6) 68 | diff-lcs (1.3) 69 | erubis (2.7.0) 70 | factory_bot (4.8.2) 71 | activesupport (>= 3.0.0) 72 | factory_bot_rails (4.8.2) 73 | factory_bot (~> 4.8.2) 74 | railties (>= 3.0.0) 75 | ffi (1.9.18) 76 | globalid (0.4.1) 77 | activesupport (>= 4.2.0) 78 | hashie (3.5.6) 79 | i18n (0.9.1) 80 | concurrent-ruby (~> 1.0) 81 | jsonapi-renderer (0.2.0) 82 | listen (3.1.5) 83 | rb-fsevent (~> 0.9, >= 0.9.4) 84 | rb-inotify (~> 0.9, >= 0.9.7) 85 | ruby_dep (~> 1.2) 86 | loofah (2.1.1) 87 | crass (~> 1.0.2) 88 | nokogiri (>= 1.5.9) 89 | mail (2.7.0) 90 | mini_mime (>= 0.1.1) 91 | method_source (0.9.0) 92 | mini_mime (1.0.0) 93 | mini_portile2 (2.3.0) 94 | minitest (5.10.3) 95 | nio4r (2.1.0) 96 | nokogiri (1.8.1) 97 | mini_portile2 (~> 2.3.0) 98 | omniauth (1.7.1) 99 | hashie (>= 3.4.6, < 3.6.0) 100 | rack (>= 1.6.2, < 3) 101 | orm_adapter (0.5.0) 102 | pg (0.21.0) 103 | pry (0.11.3) 104 | coderay (~> 1.1.0) 105 | method_source (~> 0.9.0) 106 | pry-byebug (3.5.1) 107 | byebug (~> 9.1) 108 | pry (~> 0.10) 109 | pry-rails (0.3.6) 110 | pry (>= 0.10.4) 111 | pry-stack_explorer (0.4.9.2) 112 | binding_of_caller (>= 0.7) 113 | pry (>= 0.9.11) 114 | puma (3.11.0) 115 | rack (2.0.3) 116 | rack-attack (5.0.1) 117 | rack 118 | rack-cors (1.0.2) 119 | rack-protection (2.0.0) 120 | rack 121 | rack-test (0.6.3) 122 | rack (>= 1.0) 123 | rails (5.0.6) 124 | actioncable (= 5.0.6) 125 | actionmailer (= 5.0.6) 126 | actionpack (= 5.0.6) 127 | actionview (= 5.0.6) 128 | activejob (= 5.0.6) 129 | activemodel (= 5.0.6) 130 | activerecord (= 5.0.6) 131 | activesupport (= 5.0.6) 132 | bundler (>= 1.3.0) 133 | railties (= 5.0.6) 134 | sprockets-rails (>= 2.0.0) 135 | rails-dom-testing (2.0.3) 136 | activesupport (>= 4.2.0) 137 | nokogiri (>= 1.6) 138 | rails-html-sanitizer (1.0.3) 139 | loofah (~> 2.0) 140 | railties (5.0.6) 141 | actionpack (= 5.0.6) 142 | activesupport (= 5.0.6) 143 | method_source 144 | rake (>= 0.8.7) 145 | thor (>= 0.18.1, < 2.0) 146 | rake (12.3.0) 147 | rb-fsevent (0.10.2) 148 | rb-inotify (0.9.10) 149 | ffi (>= 0.5.0, < 2) 150 | redis (4.0.1) 151 | redis-actionpack (5.0.2) 152 | actionpack (>= 4.0, < 6) 153 | redis-rack (>= 1, < 3) 154 | redis-store (>= 1.1.0, < 2) 155 | redis-activesupport (5.0.4) 156 | activesupport (>= 3, < 6) 157 | redis-store (>= 1.3, < 2) 158 | redis-rack (2.0.3) 159 | rack (>= 1.5, < 3) 160 | redis-store (>= 1.2, < 2) 161 | redis-rails (5.0.2) 162 | redis-actionpack (>= 5.0, < 6) 163 | redis-activesupport (>= 5.0, < 6) 164 | redis-store (>= 1.2, < 2) 165 | redis-store (1.4.1) 166 | redis (>= 2.2, < 5) 167 | responders (2.4.0) 168 | actionpack (>= 4.2.0, < 5.3) 169 | railties (>= 4.2.0, < 5.3) 170 | rspec-core (3.7.0) 171 | rspec-support (~> 3.7.0) 172 | rspec-expectations (3.7.0) 173 | diff-lcs (>= 1.2.0, < 2.0) 174 | rspec-support (~> 3.7.0) 175 | rspec-mocks (3.7.0) 176 | diff-lcs (>= 1.2.0, < 2.0) 177 | rspec-support (~> 3.7.0) 178 | rspec-rails (3.7.2) 179 | actionpack (>= 3.0) 180 | activesupport (>= 3.0) 181 | railties (>= 3.0) 182 | rspec-core (~> 3.7.0) 183 | rspec-expectations (~> 3.7.0) 184 | rspec-mocks (~> 3.7.0) 185 | rspec-support (~> 3.7.0) 186 | rspec-support (3.7.0) 187 | ruby_dep (1.5.0) 188 | sidekiq (5.0.5) 189 | concurrent-ruby (~> 1.0) 190 | connection_pool (~> 2.2, >= 2.2.0) 191 | rack-protection (>= 1.5.0) 192 | redis (>= 3.3.4, < 5) 193 | spring (2.0.2) 194 | activesupport (>= 4.2) 195 | spring-watcher-listen (2.0.1) 196 | listen (>= 2.7, < 4.0) 197 | spring (>= 1.2, < 3.0) 198 | sprockets (3.7.1) 199 | concurrent-ruby (~> 1.0) 200 | rack (> 1, < 3) 201 | sprockets-rails (3.2.1) 202 | actionpack (>= 4.0) 203 | activesupport (>= 4.0) 204 | sprockets (>= 3.0.0) 205 | thor (0.20.0) 206 | thread_safe (0.3.6) 207 | tzinfo (1.2.4) 208 | thread_safe (~> 0.1) 209 | warden (1.2.7) 210 | rack (>= 1.0) 211 | websocket-driver (0.6.5) 212 | websocket-extensions (>= 0.1.0) 213 | websocket-extensions (0.1.3) 214 | 215 | PLATFORMS 216 | ruby 217 | 218 | DEPENDENCIES 219 | active_model_serializers 220 | devise_token_auth 221 | factory_bot_rails 222 | listen 223 | omniauth 224 | pg 225 | pry-byebug 226 | pry-rails 227 | pry-stack_explorer 228 | puma 229 | rack-attack 230 | rack-cors 231 | rails (~> 5.0.0) 232 | redis-rails 233 | rspec-rails 234 | sidekiq 235 | spring 236 | spring-watcher-listen 237 | tzinfo-data 238 | 239 | BUNDLED WITH 240 | 1.16.0 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How to Install 2 | 3 | I have included a step by step guide in this README on how I set this up. But if you just want to get up and running, here are the steps: 4 | 5 | 1. Clone this repository 6 | 2. Rename the repo `mv rails5-api-guide/ my_app` 7 | 3. Inside your database.yml rename the database to whatever you want, otherwise it will just be named `generic_api_development`, `generic_api_test`, `generic_api_production` for each environment respectively. 8 | 4. Rename the top level module inside `application.rb` to whatever you want otherwise it will be named `GenericApi` 9 | 5. Run `bundle install` 10 | 6. Run `bundle exec rake db:create` 11 | 7. Run `bundle exec rake db:migrate; RAILS_ENV=test bundle exec rake db:migrate` 12 | 8. Boot up the server with `rails s` and navigate to localhost:3000 to see your new rails application 13 | 14 | If you are interested in how this app was setup and what is included read on. 15 | 16 | # Setting up a Rails 5 API Only Application: Step by Step Guide 17 | 18 | ## Introduction 19 | This guide shows you how I setup my default Rails architecture, now updated for Rails 5. Our Rails layer will do nothing but provide an API which serves JSON, and process background jobs. Here are some of the things we will be setting up: 20 | 21 | * Token based authentication with the `devise_token_auth` gem. 22 | * A namespaced API 23 | * Serialization with Active Model Serializers 24 | * Testing with rspec and factory girl 25 | * Useful rack middleares like rack-attack for rate limiting and throttling with rack-cors. 26 | * Local development niceties such as mailcatcher, pry/pry-nav, and useful git commit hooks 27 | * Background jobs with Sidekiq 28 | * Caching with Redis 29 | 30 | ## Initial Setup 31 | 32 | **Install Rails** 33 | 34 | `gem install rails` 35 | 36 | **Create a new API only project with Postgres as the selected database** 37 | 38 | `rails new project_name --api --database=postgresql` 39 | 40 | **Setup the database** 41 | 42 | ``` 43 | cd project_name 44 | rails db:setup 45 | rails db:migrate 46 | ``` 47 | 48 | **Confirm that the server is working** 49 | 50 | Boot up the server with the command `rails s` inside the project directory. Visit localhost:3000 and you should see a page that greets you with the message "Yay! You're on Rails!" 51 | 52 | **Add rspec, factory girl, and pry to test and development** 53 | 54 | Lets go ahead and a add a couple gems that will be very useful for testing and development. I prefer to use rspec for testing purposes, but some of you might wan't to stick with minitest, if that's the case you can just ignore the `rspec-rails` gem. If not add `rspec-rails` and `factory_bot_rails`. FactoryBot will let us easily create and mock test objects. The gem `pry-rails` provides a very handy interactive console when using rails c, `pry-byebug` gives you powerful break points and `pry-stack_expolorer` rounds out the package with a very solid stack explorer. 55 | 56 | Open Gemfile, add the following. You can get rid of whatever else was in the :development, :test group 57 | 58 | ``` 59 | group :development, :test do 60 | gem 'rspec-rails' 61 | gem 'factory_bot_rails' 62 | gem 'pry-rails' 63 | gem 'pry-byebug' 64 | gem 'pry-stack_explorer' 65 | end 66 | ``` 67 | 68 | Install the gems by running `bundle install`. 69 | 70 | Finish the rspec installation by running the command `rails generate rspec:install`. 71 | 72 | You can go ahead and remove the test directory since we will be using /spec 73 | 74 | `rm -rf test` 75 | 76 | Now would be a good time to put everything under version control 77 | 78 | ``` 79 | git init 80 | git add --all 81 | git commit -m 'Initial Commit' 82 | ``` 83 | 84 | Set up your git remotes if you would like. I trust you know how to do this. 85 | 86 | # Setting up token based authentication 87 | 88 | Add `devise_token_auth` and `omniauth` gem to the Gemfile: 89 | 90 | ``` 91 | gem 'devise_token_auth' 92 | gem 'omniauth' 93 | ``` 94 | 95 | Run `bundle install` to install the gems. 96 | 97 | **Generating the user model with devise concerns and routes** 98 | 99 | `rails g devise_token_auth:install User auth` 100 | 101 | The devise generator accepts two arguments the user class (in our case User) which is the name of the class to use for user authentication. The second argument is the mount path (in our case auth) which is path at which to mount the authentication routes. 102 | 103 | The generator will create an initializer at `config/initializers/devise_token_auth.rb`, a User model at `app/models/user.rb`, a concern will be included in `app/controllers/application_controller.rb`, routes defined in `config/routes.rb`, and a migration to create the `users` table. You may want to tweak the migration to add columns to your liking. See [the github page](https://github.com/lynndylanhurley/devise_token_auth) for more information. 104 | 105 | Run `rails db:migrate` to add the users table as defined by the devise migrations. 106 | 107 | Now run `rails routes` to see the authentication routes that have been setup. 108 | 109 | **Mailer Configuration** 110 | 111 | Since devise uses mailers for account registration, deletion, password changing, etcetera, we need to configure our development environment to send mail. The first thing is we need to add the following to `config/initializers/devise.rb` 112 | 113 | ``` 114 | Devise.setup do |config| 115 | # Using rails-api, tell devise to not use ActionDispatch::Flash 116 | # middleware b/c rails-api does not include it. 117 | config.navigational_formats = [:json] 118 | end 119 | ``` 120 | 121 | The comment explains it all. We wan't devise to ignore the `ActionDispatch::Flash` middleware since it is not bundled in the Rails API mode middleware stack. 122 | 123 | Next Add the following to `config/environments/development.rb` 124 | 125 | ``` 126 | config.action_mailer.delivery_method = :smtp 127 | config.action_mailer.smtp_settings = { address: 'localhost', port: 1025 } 128 | config.action_mailer.default_url_options = { :host => 'localhost', port: '3000' } 129 | ``` 130 | 131 | **Token based authentication in action** 132 | 133 | Let's see some token authentication in action. Boot up your rails server with the `rails s` command. 134 | 135 | Examining the output of `rails routes` we can see that in order to regiser a new user we have to submit a POST request to the `/auth` routes. Let's do that using `curl`: 136 | 137 | `curl --data "email=test@test.com&password=testpassword1&password_confirmation=testpassword1&confirm_success_url=http://localhost:3000" http://localhost:3000/auth` 138 | 139 | You should see a response that looks like the following: 140 | 141 | `{"status":"success","data":{"id":1,"provider":"email","uid":"test@test.com","name":null,"nickname":null,"image":null,"email":"test@test.com","created_at":"2016-07-23T23:16:10.064Z","updated_at":"2016-07-23T23:16:10.064Z"}}` 142 | 143 | If you examine your server logs, you should see that the action was processed and an email response was generated: 144 | 145 | ``` 146 | Started POST "/auth" for ::1 at 2016-07-23 19:16:09 -0400 147 | Processing by DeviseTokenAuth::RegistrationsController#create as */* 148 | Parameters: {"email"=>"test@test.com", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "confirm_success_url"=>"http://localhost:3000"} 149 | -- snip -- 150 | Devise::Mailer#confirmation_instructions: processed outbound mail in 170.2ms 151 | Sent mail to test@test.com (8.4ms) 152 | Date: Sat, 23 Jul 2016 19:16:10 -0400 153 | From: please-change-me-at-config-initializers-devise@example.com 154 | Reply-To: please-change-me-at-config-initializers-devise@example.com 155 | To: test@test.com 156 | Message-ID: 157 | Subject: Confirmation instructions 158 | Mime-Version: 1.0 159 | Content-Type: text/html; 160 | charset=UTF-8 161 | Content-Transfer-Encoding: 7bit 162 | client-config: default 163 | redirect-url: http://localhost:3000 164 | 165 |

Welcome test@test.com!

166 | 167 |

You can confirm your account email through the link below:

168 | 169 |

Confirm my account

170 | 171 | Completed 200 OK in 666ms (Views: 0.6ms | ActiveRecord: 35.4ms) 172 | ``` 173 | 174 | Go ahead and copy the link into your broswer. Then, boot up a rails console using the `rails c` command, and run `User.first`. This should return the new test user you just created. 175 | 176 | **Setting up mailcatcher** 177 | 178 | Reading the server logs to see what emails were rendered is not a great dev experience. Let's set up mailcatcher so that we can intercept emails delivered locally. 179 | 180 | Install mailcatcher with the command `gem install mailcatcher`. Start mailcatcher by running `mailcatcher`. This will boot it up as a background daemon. Go to [http://localhost:1080/](http://localhost:1080/) to see the mailcatcher UI. 181 | 182 | Now lets try registering a new user. 183 | 184 | `curl --data "email=test2@test.com&password=testpassword1&password_confirmation=testpassword1&confirm_success_url=http://localhost:3000" http://localhost:3000/auth` 185 | 186 | This time we should be able to see the email in the mailcatcher UI. 187 | 188 | ## Serialization with ActiveModelSerializers 189 | Our API will be responsible for rendering JSON responses, and that's about it. In order to build these JSON responses, we will use a gem called ActiveModelSerializers. 190 | 191 | **Install ActiveModelSerializers** 192 | 193 | Add `gem 'active_model_serializers'` to the Gemfile and run `bundle install`. 194 | 195 | Add a config file in the location `config/initializers/active_model_serializer.rb` with the code: 196 | 197 | ``` 198 | ActiveModelSerializers.config.adapter = :json_api 199 | ``` 200 | 201 | **Generating a user serializer** 202 | 203 | Run the command `rails g serializer user`. In the generated user serializer add the attributes :name, and :email so that the final code looks like this: 204 | 205 | ``` 206 | class UserSerializer < ActiveModel::Serializer 207 | attributes :id, :name, :email 208 | end 209 | ``` 210 | 211 | **Setup an endpoint for serving user objects** 212 | 213 | Now that we have a user serializer, let's actually setup some endpoints that will serve us some user objects. This will be the begining of our API. 214 | 215 | Let's start by setting up the routes. We will scope these api routes to a module 'api' and then provide a v1 namespace for all our routes. Here is what we will add to the routes file: 216 | 217 | ``` 218 | scope module: 'api' do 219 | namespace :v1 do 220 | resources :users, only: [:index, :show] 221 | end 222 | end 223 | ``` 224 | 225 | Run `rails routes` to see the routes that have been set up. You should see something like this: 226 | 227 | ``` 228 | v1_users GET /v1/users(.:format) api/v1/users#index 229 | v1_user GET /v1/users/:id(.:format) api/v1/users#show 230 | ``` 231 | 232 | Now let's add an api controller from which all our other controllers in the API will inherit. The file should live in`app/controllers/api/v1/api_controller.rb`. 233 | 234 | ``` 235 | module Api::V1 236 | class ApiController < ApplicationController 237 | # before_action :authenticate_user! 238 | end 239 | end 240 | ``` 241 | 242 | Next add a users controller, with two actions index and show: `app/controllers/api/v1/users_controller.rb` 243 | 244 | ``` 245 | module Api::V1 246 | class UsersController < ApiController 247 | 248 | # GET /v1/users 249 | def index 250 | render json: User.all 251 | end 252 | 253 | # GET /v1/users/{id} 254 | def show 255 | render json: User.find(params[:id]) 256 | end 257 | 258 | end 259 | end 260 | ``` 261 | 262 | Now lets confirm that our controllers, routes, and serialization is working as expected: 263 | 264 | Run `curl http://localhost:3000/v1/users`, which will be routed to our index action, and you should see a response that looks something like this: 265 | 266 | ``` 267 | {"data":[{"id":"1","type":"users","attributes":{"name":null,"email":"test@test.com"}},{"id":"2","type":"users","attributes":{"name":null,"email":"test2@test.com"}}]} 268 | ``` 269 | 270 | Run `curl http://localhost:3000/v1/users/1`, which will be routed to our show action, and you should see a response like this: 271 | 272 | ``` 273 | {"data":{"id":"1","type":"users","attributes":{"name":null,"email":"test@test.com"}}} 274 | ``` 275 | Great! We now have a bare minimum working API that serves some JSON. We can see that the attributes being returned are only the ones we specified in the serializer. 276 | 277 | ## Authenticating our API 278 | 279 | We setup devise, but we are not actually authenticating anywhere in our API! Let's do that now. 280 | 281 | Devise makes it easy for us. All we have to do is add the following `before_action` to `inapp/controllers/api/v1/api_controller.rb`: 282 | 283 | ``` 284 | before_action :authenticate_user! 285 | ``` 286 | 287 | Since we are poking around in our base api controller, and since we will exclusively be serving JSON to our clients, we can add the following to api controller: 288 | 289 | ``` 290 | respond_to :json 291 | ``` 292 | 293 | Great! Now if we try to hit the users controller with the same curl commands we used above we should get an authentication error. Let's test it out. Run `curl http://localhost:3000/v1/users/1`, and you should get the response 294 | 295 | ``` 296 | {"errors":["Authorized users only."]} 297 | ``` 298 | 299 | **Using tokens for authentication** 300 | 301 | When using token based authentication, we have to generate a token for every single request. Tokens are invalidated after each request to the API. During each request, a new token is generated. The access-token header that should be used in the next request is returned in the access-token header of the response to the previous request. For more information see the [devise_token_auth documentation](https://github.com/lynndylanhurley/devise_token_auth#about-token-management). 302 | 303 | When we run `rails routes` we should see a devise route that looks like this: 304 | 305 | ``` 306 | user_session POST /auth/sign_in(.:format) devise_token_auth/sessions#create 307 | ``` 308 | 309 | This is our login route. It return the initial auth token for when a user signs in. Let's test that by sending a post request to the endpoint with the appropriate payload: 310 | 311 | ``` 312 | curl -XPOST -v -H 'Content-Type: application/json' localhost:3000/auth/sign_in -d '{"email": "test@test.com", "password": "testpassword1" }' 313 | ``` 314 | 315 | We should get a response that looks like this: 316 | 317 | ``` 318 | * Connected to localhost (::1) port 3000 (#0) 319 | > POST /auth/sign_in HTTP/1.1 320 | -- snip -- 321 | < Content-Type: application/json; charset=utf-8 322 | < access-token: Avj8j2wQ4JAlFUDuPbS3fQ 323 | < token-type: Bearer 324 | < client: r4Pn4MLXvpCFTkwSc0HD7w 325 | < expiry: 1470579487 326 | < uid: test@test.com 327 | -- snip -- 328 | * Connection #0 to host localhost left intact 329 | {"data":{"id":1,"email":"dnasseri10@gmail.com","provider":"email","uid":"test@test.com","name":null,"nickname":null,"image":null,"type":"user"}} 330 | ``` 331 | 332 | As you can see, we get the access-token, client, and uid in the headers of the response. The JSON response provides us with some attributes on the user object. Since this is a default devise route, it does not go through the user serializer we setup. 333 | 334 | Now we can use the validate_token route to confirm that our token is, well, valid: 335 | 336 | ``` 337 | curl -XGET -v -H 'Content-Type: application/json' -H 'access-token: Avj8j2wQ4JAlFUDuPbS3fQ' -H 'client: r4Pn4MLXvpCFTkwSc0HD7w' -H "uid: test@test.com" localhost:3000/auth/validate_token` 338 | ``` 339 | 340 | The success response: 341 | 342 | ``` 343 | * Connected to localhost (::1) port 3000 (#0) 344 | > GET /auth/validate_token HTTP/1.1 345 | -- snip -- 346 | < Content-Type: application/json; charset=utf-8 347 | < access-token: 2J6ygFVQrYHGy6aSH25D_g 348 | < token-type: Bearer 349 | < client: r4Pn4MLXvpCFTkwSc0HD7w 350 | < expiry: 1470579646 351 | < uid: test@test.com 352 | -- snip -- 353 | {"success":true,"data":{"id":1,"provider":"email","uid":"test@test.com","name":null,"nickname":null,"image":null,"email":"test@test.com","type":"user"}} 354 | ``` 355 | 356 | You can see the new access was generated and sent back in the headers of the previous response. So let's hit the API with the token from the last request! 357 | 358 | ``` 359 | curl -XGET -v -H 'Content-Type: application/json' -H 'access-token: 2J6ygFVQrYHGy6aSH25D_g' -H 'client: r4Pn4MLXvpCFTkwSc0HD7w' -H "uid: test@test.com" localhost:3000/v1/users/ 360 | ``` 361 | 362 | And the sucessful response should look as follows: 363 | 364 | ``` 365 | {"data":[{"id":"2","type":"users","attributes":{"name":null,"email":"test@test.com"}},{"id":"1","type":"users","attributes":{"name":null,"email":"test2@test.com"}}]} 366 | ``` 367 | 368 | Fantastic. At this point we have a bare minimum rails 5 api. Our routes are namespaced, users are authenticated with token based auth, and can sign up with email. We serialize our responses using `active_model_serializers`. This is most of what we need to get up and running. 369 | 370 | ## Setting Up Specs 371 | 372 | We sent some time setting up rspec and some useful debugging tools like pry-nav and factory girl, but we haven't actually written any tests yet. Why don't we go ahead and set up our first spec. 373 | 374 | First we need to set up factory bot. At the top of our `spec/spec_helper.rb` we need to explicitly require it as follows: `require 'factory_girl_rails'`. Then to gain access to the FactoryBot DSL inside our specs we need to add the following line `config.include FactoryBot::Syntax::Methods` within our config block. Then inside of our `.rspec` file we need to add the following line `--require rails_helper`. We can go ahead and get rid of the line `--require spec_helper`, since the `rails_helper` includes the `spec_helper`. If we don't do this RSpec Cannot find the Controllers and throws an Uninitialized Constant error. See [here](http://stackoverflow.com/questions/26288113/rspec-cannot-find-my-controllers-uninitialized-constant) for more information. 375 | 376 | Now lets set up a very simple test for the users controller in the location `spec/controllers/api/v1/users_controller_spec.rb`. 377 | 378 | ``` 379 | require 'spec_helper' 380 | 381 | describe Api::V1::UsersController do 382 | describe "GET #show" do 383 | before(:each) do 384 | @user = FactoryBot.create :user 385 | auth_headers = @user.create_new_auth_token 386 | request.headers.merge!(auth_headers) 387 | get :show, id: @user.id 388 | end 389 | 390 | it 'responds with 200 status code' do 391 | expect(response.code).to eq('200') 392 | end 393 | end 394 | end 395 | ``` 396 | 397 | As you can see in the before block we hadd to generate an auth token for our user, and merge it into the request headers. See [here](https://github.com/lynndylanhurley/devise_token_auth/issues/75) for more information. Since you will likely be doing this in all your controller tests, it might be useful to abstract this to a convienience method inside `spec_helper`, for now I will leave it. 398 | 399 | When I tried running the test by executing `rspec spec/controllers/api/v1/users_controller_spec.rb` I got the following error: 400 | 401 | ``` 402 | Devise::MissingWarden: 403 | Devise could not find the `Warden::Proxy` instance on your request environment. 404 | Make sure that your application is loading Devise and Warden as expected and that the `Warden::Manager` middleware is present in your middleware stack. 405 | If you are seeing this on one of your tests, ensure that your tests are either executing the Rails middleware stack or that your tests are using the `Devise::Test::ControllerHelpers` module to inject the `request.env['warden']` object for you. 406 | ``` 407 | 408 | What a useful error message! All I had to do was add the devise test helpers to `rails_helper.rb` inside the config block: `config.include Devise::Test::ControllerHelpers, type: :controller`. 409 | 410 | Now running the test should pass! 411 | 412 | Let's flesh this test out by adding another expectation for checking the response body: 413 | 414 | ``` 415 | it "returns the serialized user attributes" do 416 | expect(JSON.parse(response.body)['data']['attributes']).to eq({"name"=>"John Doe", "email"=>"test@test.com"}) 417 | end 418 | ``` 419 | 420 | And now let's add some tests for the index action: 421 | 422 | ``` 423 | describe Api::V1::UsersController do 424 | 425 | -- snip -- 426 | 427 | describe 'GET #index' do 428 | before(:each) do 429 | @user = FactoryBot.create :user 430 | auth_headers = @user.create_new_auth_token 431 | request.headers.merge!(auth_headers) 432 | get :index, id: @user.id 433 | end 434 | 435 | it 'responds with 200 status code' do 436 | expect(response.code).to eq('200') 437 | end 438 | 439 | it 'returns the serialized user attributes' do 440 | expect(JSON.parse(response.body)['data'].length).to eq(1) 441 | expect(JSON.parse(response.body)['data'].first['attributes']).to eq({'name'=>'John Doe', 'email'=>'test@test.com'}) 442 | end 443 | end 444 | end 445 | ``` 446 | 447 | ## Rack Middlewares 448 | 449 | **Use Rack CORS to allows cross origin resource sharing** 450 | 451 | If your API will be public, you will need to enable Cross-Origin Resource Sharing. A resource makes a cross-origin HTTP request when it requests a resource from a different domain than the one which the first resource itself serves. 452 | 453 | Add `gem 'rack-cors'` to the Gemfile and run `bundle install` 454 | 455 | Then add this to `application.rb`: 456 | 457 | ``` 458 | config.middleware.use Rack::Cors do 459 | allow do 460 | origins '*' 461 | resource '*', 462 | :headers => :any, 463 | :expose => ['access-token', 'expiry', 'token-type', 'uid', 'client'], 464 | :methods => [:get, :post, :options, :delete, :put] 465 | end 466 | end 467 | ``` 468 | 469 | WARNING: This will whitelist requests from any domain. Make sure to whitelist only the needed domains. 470 | 471 | **Rate limiting/throttling, blacklisting/whitelisting of IP's with rack-attack** 472 | 473 | `rack-attack` is a middleware that allows us rate limit, throttle, white list, and black list IP's. Trust me, you want this. 474 | 475 | Add `gem 'rack-attack'` to the Gemfile and then run `bundle install`. Inside of `config/application.rb` add `config.middleware.use Rack::Attack`. 476 | 477 | Next, create a new initializer `config/initializers/rack_attack.rb`. I plucked the basic config from this [super helpful guide](http://sourcey.com/building-the-prefect-rails-5-api-only-app/#enabling-cors) 478 | 479 | ``` 480 | class Rack::Attack 481 | 482 | # `Rack::Attack` is configured to use the `Rails.cache` value by default, 483 | # but you can override that by setting the `Rack::Attack.cache.store` value 484 | Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new 485 | 486 | # Allow all local traffic 487 | safelist('allow-localhost') do |req| 488 | '127.0.0.1' == req.ip || '::1' == req.ip 489 | end 490 | 491 | # Allow an IP address to make 5 requests every 5 seconds 492 | throttle('req/ip', limit: 5, period: 5) do |req| 493 | req.ip 494 | end 495 | 496 | # Send the following response to throttled clients 497 | self.throttled_response = ->(env) { 498 | retry_after = (env['rack.attack.match_data'] || {})[:period] 499 | [ 500 | 429, 501 | {'Content-Type' => 'application/json', 'Retry-After' => retry_after.to_s}, 502 | [{error: "Throttle limit reached. Retry later."}.to_json] 503 | ] 504 | } 505 | end 506 | ``` 507 | 508 | Depending on your needs, the 5 requests every 5 seconds may be a little too strict. For instance, if you are building a web client that makes several async requests, you may want to loosen the limit a bit. 509 | 510 | ## Cacheing with Redis 511 | 512 | You may have noticed that we are using the `ActiveSupport::Cache::MemoryStore.new` to back rack-attack. This is no good! This particular store puts everything into memory in the same process, so while the cache might be blazingly fast, it disappears when the process dissapears, and it's cache data cannot be shared across processes. 513 | 514 | I like to use Redis for cacheing. Some prefer Memcached. Both have their merits. But since we will be using sidekiq for job processing which requires Redis, and we may want to use action-cable, we might as well stick with Redis. 515 | 516 | First add `gem 'redis-rails'` to your Gemfile, and then run `bundle install`. Install and start redis using instructions [here](http://redis.io/topics/quickstart). 517 | 518 | In `development.rb` get rid of the block of code that looks like this: 519 | 520 | ``` 521 | # Enable/disable caching. By default caching is disabled. 522 | if Rails.root.join('tmp/caching-dev.txt').exist? 523 | config.action_controller.perform_caching = true 524 | 525 | config.cache_store = :memory_store 526 | config.public_file_server.headers = { 527 | 'Cache-Control' => 'public, max-age=172800' 528 | } 529 | else 530 | config.action_controller.perform_caching = false 531 | 532 | config.cache_store = :null_store 533 | end 534 | ``` 535 | 536 | And then replace with this: 537 | 538 | ``` 539 | if ENV['REDIS_URL'] 540 | config.action_controller.perform_caching = true 541 | config.cache_store = :redis_store, ENV['REDIS_URL'] 542 | else 543 | config.action_controller.perform_caching = false 544 | config.cache_store = :null_store 545 | end 546 | ``` 547 | 548 | This way we can enable cacheing locally if we wish to test it. Usually in development cacheing is turned off, but if you want to enable it all you have to do is set the environment variable `REDIS_URL` to `redis://localhost:6379`. 549 | 550 | In `production.rb` add 551 | 552 | ``` 553 | config.cache_store = :redis_store, ENV['REDIS_URL'] 554 | ``` 555 | 556 | ## Background Job Processing with Sidekiq 557 | This is sort of optional, but Sideiq is my go to for background job processing in ruby. It is extremely robust and efficient. 558 | 559 | 560 | #### Getting Started 561 | Add `gem 'sidekiq'` to the Gemfile and bundle. 562 | 563 | Create an `app/workers` directory, and try running a sample job: 564 | 565 | ``` 566 | class SampleWorker 567 | include Sidekiq::Worker 568 | def perform() 569 | # do something 570 | end 571 | end 572 | ``` 573 | 574 | You can kick it off in your console with `SampleWorker.perform_async` which will return a job ID. 575 | 576 | 577 | ## Caching 578 | 579 | http://www.victorareba.com/tutorials/speed-your-rails-app-with-model-caching-using-redis 580 | https://medium.com/ruby-on-rails/easy-caching-with-rails-4-heroku-redis-5fb36381628#.o1a12hbzb 581 | 582 | 583 | ## Nice to Haves 584 | 585 | **Setup Rack-attack to use Default Cache** 586 | 587 | Now that we have enabled redis we can enable rack-attack to use the default cache. In `config/initializers/rack_attack.rb` remove the line `Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new`. [By default, Rack::Attack will use whichever caching backend is configured as Rails.cache.](https://github.com/kickstarter/rack-attack/issues/102) 588 | 589 | **Set up a git commit hook to stop from committing binding.pry** 590 | 591 | I use binding.pry all over the place, and sometimes I forget to take it out before commiting. Here's a little pre-commit hook taken from the following [gist](https://gist.github.com/alexbevi/3436040) to prevent that from happening. 592 | 593 | Inside the `/.git/hooks/pre-commit` file 594 | 595 | ``` 596 | #!/bin/bash 597 | 598 | FILES_PATTERN='\.(rb)(\..+)?$' 599 | FORBIDDEN='binding.pry' 600 | 601 | git diff --cached --name-only | \ 602 | grep -E $FILES_PATTERN | \ 603 | GREP_COLOR='4;5;37;41' xargs grep --color --with-filename -n $FORBIDDEN && \ 604 | echo 'COMMIT REJECTED' && \ 605 | exit 1 606 | 607 | exit 0 608 | ``` 609 | 610 | Make it executable by running `chmod +x /.git/hooks/pre-commit` 611 | 612 | Now if you try to commit a binding pry it will reject the commit! Awesome! 613 | 614 | **Get rid of the "Yay! You're on Rails!" splash page** 615 | 616 | I like rails a whole bunch. But I don't need the world to know that when they go to my root url. For now let's just replace that with an empty JSON response. 617 | 618 | Inside our `routes.rb` file lets add a root: 619 | 620 | ``` 621 | root to: 'home#show' 622 | ``` 623 | 624 | And then add the following controller: `app/controllers/home_controller.rb` with 625 | 626 | ``` 627 | class HomeController < ApplicationController 628 | def show 629 | render json: {} 630 | end 631 | end 632 | ``` 633 | 634 | **Change the sent from email in devise mailers** 635 | 636 | You may have noticed the "please-change-me-at-config-initializers-devise@example.com" address in the from field of our devise mailers. We can change that by specifying the `mailer_sender` inside the devise config file: 637 | 638 | ``` 639 | Devise.setup do |config| 640 | --snip-- 641 | config.mailer_sender = "support@myapp.com" 642 | 643 | end 644 | ``` 645 | --------------------------------------------------------------------------------