Episodes

Resources

Download Source Code

Summary

# Terminal
rails new template --api
rails g model User name email password_digest
rails g controller sessions create
rails g scaffold products name description
rails db:migrate

# user.rb
class User < ApplicationRecord
  has_secure_password
  # password
  # password_confirmation
end

# Gemfile
gem 'bcrypt', '~> 3.1.7'
gem 'jwt'

# lib/json_web_token.rb
class JsonWebToken
  SECRET = Rails.application.credentials.secret_key_base
  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET)
  end

  def self.decode(token)
    body = JWT.decode(token, SECRET)[0]
    HashWithIndifferentAccess.new(body)
  rescue JWT::ExpiredSignature
    nil
  rescue
    nil
  end
end

# lib/errors.rb
class Errors < Hash
  def add(key, value, _opts = {})
    self[key] ||= []
    self[key] << value
    self[key].uniq!
  end

  def add_multiple_errors(errors_hash)
    errors_hash.each do |key, values|
      values.each { |value| add key, value }
    end
  end

  def each
    each_key do |field|
      self[field].each { |message| yield field, message }
    end
  end
end

# config/application.rb
module Template
  class Application < Rails::Application
    ...
    config.autoload_paths << Rails.root.join('lib')
    ...
    config.api_only = true
  end
end

# services/application_service.rb
class ApplicationService
  # AuthenticateUser.call(arg)
  
  # def self.call(*arg)
  class << self
    def call(*arg)
      new(*arg).constructor
    end
  end
  
  attr_reader :result
  def constructor
    @result = call
    self
  end

  def success?
    !failure?
  end

  def failure?
    errors.any?
  end

  def errors
    @errors ||= Errors.new
  end

  def call
    fail NotImplementedError unless defined?(super)
  end
end

# services/authenticate_user.rb
class AuthenticateUser < ApplicationService
  def initialize(email, password)
    @email = email
    @password = password
  end
  
  private 
  
  attr_accessor :email, :password

  def call
    JsonWebToken.encode(user_id: user.id) if user
  end

  def user
    user = User.find_by(email: email)
    return user if user&.authenticate(password)
    errors.add :user_authentication, 'invalid credentials'
  end
end

# services/authorize_api_request.rb
class AuthorizeApiRequest < ApplicationService
  attr_reader :headers

  def initialize(headers = {})
    @headers = headers
  end

  def call
    user
  end

  private

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
    @user || errors.add(:token, 'invalid token') && nil
  end

  def decoded_auth_token
    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
  end

  def http_auth_header
    if headers['Authorization'].present?
      return headers['Authorization'].split(' ').last
    else
      errors.add(:token, 'missing token')
    end
    nil
  end
end

# routes.rb
Rails.application.routes.draw do
  resources :products
  post 'authenticate', to: 'sessions#create'
end

# application_controller.rb
class ApplicationController < ActionController::API
  before_action :authenticate_request
  attr_reader :current_user

  private

  def authenticate_request
    auth = AuthorizeApiRequest.call(request.headers)
    @current_user = auth.result
    render json: { errors: auth.errors }, status: :unauthorized unless @current_user
  end
end

# sessions_controller.rb
class SessionsController < ApplicationController
  skip_before_action :authenticate_request
  def create
    auth = AuthenticateUser.call(params[:email], params[:password])
    if auth.success?
      render json: { auth_token: auth.result }
    else
      render json: { errors: auth.errors }, status: :unauthorized
    end
  end
end

# Terminal
curl -H 'Content-Type: application/json' -X POST -d '{"email":"[email protected]","password":"123456"}' http://localhost:3000/authenticate

# => {"auth_token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjkzODY3NDl9.8T18REGfJMDbax8-VvTOkmswfw_-aVGXwFYoHzyeFTw"}%

curl -H 'Content-Type: application/json' -H 'Authorization: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE1MjkzODY3NDl9.8T18REGfJMDbax8-VvTOkmswfw_-aVGXwFYoHzyeFTw' http://localhost:3000/products

# => [{"id":1,"name":"Computer","description":null,"created_at":"XXXX","updated_at":"XXXX"},
      {"id":2,"name":"Phone","description":null,"created_at":"XXXX","updated_at":"XXXX"},
      {"id":3,"name":"Fax","description":null,"created_at":"XXXX","updated_at":"XXXX"}]