6 months ago

rails 5.0, devise 4.0
thanks for gist
and devise gem override controller

Part I: Add api

$git branch api_v1
$git checkout api_v1
$rails generate controller api/v1/battles

and copy battles_controller to the new file with some additional code

class Api::V1::BattlesController < ApplicationController
  before_action :authenticate_user!, only: [:follow_left_video, :unfollow_left_video, :follow_right_video, :unfollow_right_video]
  before_action :find_battle, only: [:follow_left_video, :unfollow_left_video, :follow_right_video, :unfollow_right_video]

  def index
    respond_to :json
    @battle = Battle.recent.first

and create file battles.json.jbuilder under app/view/api/v1/battles with content

if @battle.present?
  json.extract! @battle, :id, :title, :description, :left_video_id, :right_video_id
else
  return "no battle"
end

and add new content in route.rb

  namespace :api do
    namespace :v1 do
      resources :battles do
        member do
          post :follow_left_video
          post :unfollow_left_video
          post :follow_right_video
          post :unfollow_right_video
        end

    # resources :videos do
    #   resources :video_comments, only: [:new, :create]
    # end
      resources :battle_comments, only: [:new, :create]
      end
    end
  end

we can test it by input xxx.xx/api/v1/battles.json in browser.

$ git add .
$ git commit -m"api done"
$ git checkout master

Part II: add json login based on devise

Step 1: override devise

from devise gem devise gem override controller

$ git checkout master
$ git branch override_devise
$ git checkout override_devise
$ rails generate devise:controllers users

update devise content in route.rb

Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: 'users/sessions'
  }
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root 'battles#index'

test it now, the user management should still work like before.

Step2 add new gem simple_token_authentication

# Gemfile
gem 'simple_token_authentication', '~> 1.0' # see semver.org

and add one more attribute for user model

$ bundle install
$ rails g migration add_authentication_token_to_users "authentication_token:string{30}:uniq"
$ rails db:migrate

add following code to user.rb in model
on top of user.rb

# app/models/user.rb

class User < ActiveRecord::Base
  acts_as_token_authenticatable
  # Note: you can include any module you want. If available,
  # token authentication will be performed before any other
  # Devise authentication method.
  #
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :invitable, :database_authenticatable,
         :recoverable, :rememberable, :trackable, :validatable,
         :lockable

  # ...

on bottome of user.rb

   def reset_authentication_token!
     self.authentication_token = generate_authentication_token("")
     self.save
   end
 
  private
  def generate_authentication_token(token_generator)
    loop do
      token = Devise.friendly_token
      break token unless User.where(authentication_token: token).first
    end
  end

terst login and logout again, should still work well.

now update app/controllers/users/sessions_controllers.rb

class Users::SessionsController < Devise::SessionsController
  # before_action :configure_sign_in_params, only: [:create]
  skip_before_action :verify_authenticity_token, if: :json_request?


  def json_request?
    request.format.json?
  end
  # GET /resource/sign_in
  # def new
  #   super
  # end

  # POST /resource/sign_in
  def create
    if json_request?
      email = params[:email] if params[:appid]
      password = params[:password] if params[:appsecret]

      id = User.find_by(email: email).try(:id) if email.presence

      # Validations
      if request.format != :json
        render status: 406, json: { message: 'The request must be JSON.' }
        return
      end

      if email.nil? or password.nil?
        render status: 400, json: { message: 'The request MUST contain the user email and password.' }
        return
      end

      # Authentication
      user = User.find_by(email: email)

      if user
        if user.valid_password? password
          user.reset_authentication_token!
          # Note that the data which should be returned depends heavily of the API client needs.
          render status: 200, json: { email: user.email, authentication_token: user.authentication_token, id: id }
        else
          render status: 401, json: { message: 'Invalid email or password.' }
        end
      else
        render status: 401, json: { message: 'Invalid email or password.' }
      end
    else
      #if json_request? == false
      super
    end
  end

  # DELETE /resource/sign_out
  def destroy
    if json_request?

      # Fetch params
      user = User.find_by(authentication_token: params[:user_token])

      if user.nil?
        render status: 404, json: { message: 'Invalid token.' }
      else
        user.authentication_token = nil
        user.save!
        render status: 204, json: nil
      end
    else
      super
    end
  end

  # protected

  # If you have extra params to permit, append them to the sanitizer.
  # def configure_sign_in_params
  #   devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
  # end
end

now we support json login, following is test code in python

import requests
rooturl = "https://lin-rails-tt-myrual.c9users.io"
loginRes = requests.post(rooturl+ "/users/sign_in.json", params={'appid':'app123', 'appsecret':'333', 'email':'admin@test.com', 'password':'123456'})
print(loginRes.text)

Step3 merge json api and simple token

git add .
git commit -m"json login done"
git merge api_v1

test all web browser login and json login again.

now we add protect to api/v1/battles controller

class Api::V1::BattlesController < ApplicationController
  #before_action :authenticate_user!, only: [:follow_left_video, :unfollow_left_video, :follow_right_video, :unfollow_right_video]
  before_action :find_battle, only: [:follow_left_video, :unfollow_left_video, :follow_right_video, :unfollow_right_video]
  #acts_as_token_authentication_handler_for User, fallback: :none
  acts_as_token_authentication_handler_for User , only: [:index, :show, :follow_left_video, :unfollow_left_video, :follow_right_video, :unfollow_right_video]

  def index

and verify in browser again, we should see error now.

now it is time to verify with python code

import requests
rooturl = "https://lin-rails-tuto2-myrual.c9users.io"
failedRes = requests.get(rooturl+"/api/v2/battles.json")
print(failedRes.text)
loginWithoutAppidRes = requests.post(rooturl+ "/users/sign_in.json", params={'appsecret':'333', 'email':'admin@test.com', 'password':'123456'})
print(loginWithoutAppidRes.text)
raw_input()

loginRes = requests.post(rooturl+ "/users/sign_in.json", params={'appid':'app123', 'appsecret':'333', 'email':'admin@test.com', 'password':'123456'})
print(loginRes.text)
auth_token = loginRes.json()['authentication_token']
successRes = requests.get(rooturl+ "/api/v2/battles.json", params={'appid':'app123', 'appsecret':'333', 'user_email':'admin@test.com', 'user_token':auth_token})
print(successRes.text)
raw_input()
failedRes = requests.get(rooturl+"/api/v2/battles.json")
print(failedRes.text)

now the url /api/v1/battles.json is protected

← linode ubuntu16.04.02 let's encrypt install process →