Rails API-only uygulama ve token authentication

Bu bir rehberdir.

Rails ile sadece API –API-only– uygulama yazmak istiyorsanız terminalde rails g proje --api ile projeyi oluşturmalısınız. (bkz: edgeguides)

Eminim rails ile uygulama geliştiren hemen hemen herkes kullanıcı işlemleri için devise kullanıyordur. Devise daha ben rails ile geliştirmeye başlamadan çok evvel TokenAuthenticatable metoduna sahipmiş fakat bu metod kaldırılmış.

Bu wiki sayfasında token ile oturum açmak için birkaç yol verilmiş. Ama API-only uygulamada her denememde CookieStore ve SessionStore hatası aldım bu gemler ile. Zaten tüm örnek uygulamalarda ve readme dökümanlarında API-only değil ActionController::Base den kullanılmış. Bir süre araştırdıktan sonra kendi tokenlerimi oluşturup uygulamaya koymaya karar verecektim ki tiddle gemini buldum. Tam aklımda kurduğum gibi çalışıyor. Şimdi bu gem ile API uygulamamı nasıl hazırladığımı anlatacağım.

API-only uygulama oluşturmak

rails new api-only-app -T -B -d postgresql --api

Bu komut ile oluşturduğunuz rails uygulamasınında ApplicationController ActionController::Base sınıfından değil ActionController::API sınıfından extend edilmiş olacak ve içinde csrf_token kontrolü yapan protect_from_forgery with: :exception şeklindeki middleware olmayacak. assets dizini hiç olmayacak. config/application.rb içinde config.api_only = true şeklinde bir set göreceksiniz. Gemfile içinde sass, coffee, uglifier, jquery vb assetler ile alakalı hiç bir gem olmayacak.

Öncelikle gerekli gemleri kuralım. API uygulamanıza farklı domainlerden erişim olacak ise rack-cors kuracağız. Authentication için devise + tiddle kullanacağız. Hazırsak başlayayım. Gemfile'ı aşağıdaki gibi düzenleyelim.

# Gemfile
gem 'rack-cors', :require => 'rack/cors'

gem 'devise'
gem 'tiddle'

Ve şimdi bundle install.

Gemler kurulduktan sonra…

 Cors'u ayarlayalım

config/initializers/cors.rb adında bir initializer oluşturup içeriğini aşağıdaki şekilde düzenleyelim.

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      expose: ['X-USER-TOKEN', 'X-USER-EMAIL'],
      methods: [:get, :post, :put, :delete, :options]
  end
end

Bu kadar.. 😊

Devise'ı hazırlayalım

rails g devise:install
rails g devise User

API-only uygulamada tüm requestlerin sonucu json döneceği için devise'ı aşağıdaki şekilde yapılandırmalıyız.

# config/initializers/devise.rb
Devise.setup do |config|
    config.navigational_formats = []
end

Önemli: Bazı kaynaklarda config.navigational_formats: [:json] tanımlanıyor. Ama boş array olarak tanımlanmazsa flash message hataları alıyoruz

TokenAuthenticatable için user.rb modeline aşağıdaki gibi :token_authenticatable ekleyelim.

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :trackable, :validatable,
         :token_authenticatable
  # ...
end

Tiddle için AuthenticationToken modelini oluşturalım.

rails g model AuthenticationToken body:string user:references last_used_at:datetime ip_address:string user_agent:string

Ve şimdi rake db:migrate.

Bir model oluşturalım

Örnek için scaffold üzerinden ilerleyeceğim.

rails g scaffold post title:string body:text --api

Ve şimdi rake db:migrate.

Controller dosyalarını düzenleyelim

Scaffold ile oluşturduğumuz controller dosyasını controllers/api/v1 içine taşıyalım.

mv app/controllers/posts_controller.rb app/controllers/api/v1/posts_controller.rb

Taşıdıktan sonra controller adını da aşağıdaki gibi değiştirmeyi unutmayalım ve before_action callback ayarlayarak create, update, destroy metodların koruyalım.

class Api::V1::PostsController < ApplicationController
    before_action :authenticate_user!, only: [:create, :update, :destroy]
    # ...
end

Devise metodlarının üzerine yazmak için api/auth içinde sessions_controller.rb ve registrations_controller.rb dosyalarını oluşturalım.

SessionsController da create metodunun üzerine yazarak authenticate den sonra bir token oluşturup döndürelim.

# sessions_controller.rb

class Api::Auth::SessionsController < Devise::SessionsController

  def create
    user = warden.authenticate!(auth_options)
    token = Tiddle.create_and_return_token(user, request)
    render json: { authentication_token: token }
  end

  def destroy
    Tiddle.expire_token(current_user, request) if current_user
    render json: {}
  end

  private

    # bu metod before_destroy callback i olarak çalışıyor. biz bunun üzerine yazıyoruz.
    def verify_signed_out_user
    end

end

RegistrationsController da flash mesajları yok edelim.

# registrations_controller.rb

class Api::Auth::RegistrationsController < Devise::RegistrationsController

  def create
    build_resource(sign_up_params)

    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        # set_flash_message! :notice, :signed_up if is_navigational_format?
        sign_up(resource_name, resource)
        return render :json => {:success => true, :data => resource}
      else
        # set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
        expire_data_after_sign_in!
        return render :json => {:success => true, :data => resource}
      end
    else
      clean_up_passwords resource
      return render :json => {:success => false, :error => resource.errors}
    end
  end

end

Rotalarımızı ayarlayalım

Authentication için api/auth ve uygulama rotaları için /api/v1/controller şeklinde rota kullanacağım.

Rails.application.routes.draw do

  devise_for :users,
              path: 'api/auth',
              defaults: { format: :json },
              controllers: {
                sessions: 'api/auth/sessions',
                registrations: 'api/auth/registrations'
              }

  namespace :api, defaults: { format: :json } do
    namespace :v1 do
      resources :posts
    end
  end

end

Çalıştıralım

rails s -p 3000

En basit haliyle bir API-only uygulama yazmış ve çalıştırmış olduk. Şimdi bu uygulamayı test edelim.

# kayıt olmak için

curl -X POST http://localhost:3000/api/auth --data '{user:{email:'yahya@kemal.im',password:'12345678',password_confirmation:'12345678'}}'
# sonuç
{
  "success": true,
  "data": {
    "id": 6,
    "email": "yahya@kemal.im",
    "created_at": "2017-03-30T23:13:23.879Z",
    "updated_at": "2017-03-30T23:13:23.918Z"
  }
}

# giriş yapmak ve token almak için

curl -X POST http://localhost:3000/api/auth/sign_in --data '{user:{email:'yahya@kemal.im',password:'12345678'}}'
# sonuç
{
  "authentication_token": "yYZpAEa3kxid3E6ZLHQp"
}

# posts#index için

curl -X GET http://localhost:3000/api/v1/posts

# sonuç
{
  "success": true,
  "data": {
    "id": 1,
    "title": "Test 1",
    "created_at": "2017-03-30T23:13:23.879Z",
    "updated_at": "2017-03-30T23:13:23.918Z"
  }
}

# posts#create için

curl -X POST http://localhost:3000/api/v1/posts \
  --header 'x-user-email: yahya@kemal.im' \
  --header 'x-user-token: yYZpAEa3kxid3E6ZLHQp' \
  --data '{posts:{title:"Test 2"}}'

# sonuç
{
  "success": true,
  "data": {
    "id": 1,
    "title": "Test 2",
    "created_at": "2017-03-30T23:13:23.879Z",
    "updated_at": "2017-03-30T23:13:23.918Z"
  }
}

Evet diğer korunan requestleri de aynı şekilde x-user-email ve x-user-token üst bilgilerini(header) göndererek güncelleyebilirsiniz.

Birilerine faydalı olduğunu umarak, esen kalın 🙏🏻